pull/11107/head
Rich Harris 2 years ago
parent 4d5db8bbf7
commit 30af1e7fa9

@ -95,14 +95,14 @@ function pause_effects(effects, controlled_anchor, callback) {
* @param {Element | Comment} anchor The next sibling node, or the parent node if this is a 'controlled' block * @param {Element | Comment} anchor The next sibling node, or the parent node if this is a 'controlled' block
* @param {number} flags * @param {number} flags
* @param {() => V[]} get_collection * @param {() => V[]} get_collection
* @param {null | ((item: V) => string)} get_key * @param {(value: V, index: number) => any} get_key
* @param {(anchor: Node, item: import('#client').MaybeSource<V>, index: import('#client').MaybeSource<number>) => void} render_fn * @param {(anchor: Node, item: import('#client').MaybeSource<V>, index: import('#client').MaybeSource<number>) => void} render_fn
* @param {null | ((anchor: Node) => void)} fallback_fn * @param {null | ((anchor: Node) => void)} fallback_fn
* @returns {void} * @returns {void}
*/ */
export function each(anchor, flags, get_collection, get_key, render_fn, fallback_fn = null) { export function each(anchor, flags, get_collection, get_key, render_fn, fallback_fn = null) {
/** @type {import('#client').EachState} */ /** @type {import('#client').EachState} */
var state = { flags, items: [] }; var state = { flags, next: null };
var is_controlled = (flags & EACH_IS_CONTROLLED) !== 0; var is_controlled = (flags & EACH_IS_CONTROLLED) !== 0;
@ -160,11 +160,12 @@ export function each(anchor, flags, get_collection, get_key, render_fn, fallback
// this is separate to the previous block because `hydrating` might change // this is separate to the previous block because `hydrating` might change
if (hydrating) { if (hydrating) {
var b_items = [];
/** @type {Node} */ /** @type {Node} */
var child_anchor = hydrate_nodes[0]; var child_anchor = hydrate_nodes[0];
/** @type {import('#client').EachItem | import('#client').EachState} */
var prev = state;
for (var i = 0; i < length; i++) { for (var i = 0; i < length; i++) {
if ( if (
child_anchor.nodeType !== 8 || child_anchor.nodeType !== 8 ||
@ -178,8 +179,10 @@ export function each(anchor, flags, get_collection, get_key, render_fn, fallback
} }
child_anchor = hydrate_anchor(child_anchor); child_anchor = hydrate_anchor(child_anchor);
b_items[i] = create_item(child_anchor, array[i], keys?.[i], i, render_fn, flags); var item = create_item(child_anchor, prev, null, array[i], keys?.[i], i, render_fn, flags);
child_anchor = /** @type {Comment} */ (child_anchor.nextSibling); child_anchor = /** @type {Comment} */ (child_anchor.nextSibling);
prev = item;
} }
// remove excess nodes // remove excess nodes
@ -190,12 +193,10 @@ export function each(anchor, flags, get_collection, get_key, render_fn, fallback
child_anchor = next; child_anchor = next;
} }
} }
state.items = b_items;
} }
if (!hydrating) { if (!hydrating) {
reconcile(array, state, anchor, render_fn, flags, keys); reconcile(array, state, anchor, render_fn, flags, get_key);
} }
if (fallback_fn !== null) { if (fallback_fn !== null) {
@ -229,178 +230,71 @@ export function each(anchor, flags, get_collection, get_key, render_fn, fallback
* @param {Element | Comment | Text} anchor * @param {Element | Comment | Text} anchor
* @param {(anchor: Node, item: import('#client').MaybeSource<V>, index: number | import('#client').Source<number>) => void} render_fn * @param {(anchor: Node, item: import('#client').MaybeSource<V>, index: number | import('#client').Source<number>) => void} render_fn
* @param {number} flags * @param {number} flags
* @param {any[]} keys * @param {(value: V, index: number) => any} get_key
* @returns {void} * @returns {void}
*/ */
function reconcile(array, state, anchor, render_fn, flags, keys) { function reconcile(array, state, anchor, render_fn, flags, get_key) {
var a_items = state.items; var first = state.next;
var current = first;
var lookup = new Map();
var a = a_items.length; while (current) {
var b = array.length; lookup.set(current.k, current);
current = current.next;
/** @type {Array<import('#client').EachItem>} */
var b_items = Array(b);
var is_animated = (flags & EACH_IS_ANIMATED) !== 0;
var should_update = (flags & (EACH_ITEM_REACTIVE | EACH_INDEX_REACTIVE)) !== 0;
var is_controlled = (flags & EACH_IS_CONTROLLED) !== 0;
var start = 0;
var item;
/** @type {import('#client').Effect[]} */
var to_destroy = [];
/** @type {Array<import('#client').EachItem>} */
var to_animate = [];
// Step 1 — trim common suffix
while (a > 0 && b > 0 && a_items[a - 1].k === keys[b - 1]) {
item = b_items[--b] = a_items[--a];
anchor = get_first_child(item);
resume_effect(item.e);
if (should_update) {
update_item(item, array[b], b, flags);
}
if (is_animated) {
item.a?.measure();
to_animate.push(item);
} }
}
// Step 2 — trim common prefix
while (start < a && start < b && a_items[start].k === keys[start]) {
item = b_items[start] = a_items[start];
resume_effect(item.e); current = first;
if (should_update) { /** @type {import('#client').EachState | import('#client').EachItem} */
update_item(item, array[start], start, flags); var prev = state;
}
if (is_animated) {
item.a?.measure();
to_animate.push(item);
}
start += 1; for (let i = 0; i < array.length; i += 1) {
} var value = array[i];
var key = get_key(value, i);
// Step 3 — update /** @type {import('#client').EachItem} */
if (start === a) { var item;
// add only
while (start < b) {
item = create_item(anchor, array[start], keys[start], start, render_fn, flags);
b_items[start++] = item;
}
} else if (start === b) {
// remove only
while (start < a) {
to_destroy.push(a_items[start++].e);
}
} else {
// reconcile
var moved = false;
var sources = new Int32Array(b - start);
var indexes = new Map();
var i;
var index;
var last_item;
// store the indexes of each item in the new world if (!current) {
for (i = start; i < b; i += 1) { // append
sources[i - start] = NEW_ITEM; item = create_item(anchor, prev, prev.next, value, key, i, render_fn, flags);
map_set(indexes, keys[i], i); prev = item;
current = item.next;
continue;
} }
if (is_animated) { if (key === current.k) {
// for all items that were in both the old and the new list, // noop
// measure them and store them in `to_animate` so we can current = current.next;
// apply animations once the DOM has been updated continue;
for (i = 0; i < a_items.length; i += 1) {
item = a_items[i];
if (indexes.has(item.k)) {
item.a?.measure();
to_animate.push(item);
}
}
} }
// populate the `sources` array for each old item with item = lookup.get(key);
// its new index, so that we can calculate moves
for (i = start; i < a; i += 1) {
item = a_items[i];
index = map_get(indexes, item.k);
resume_effect(item.e); if (item !== undefined) {
// move
prev.next = item;
current.prev = item;
item.prev = prev;
item.next = current;
if (index === undefined) { move(/** @type {import('#client').Dom} */ (item.e.dom), get_first_child(current));
to_destroy.push(item.e);
} else { } else {
moved = true; // create
sources[index - start] = i; item = create_item(
b_items[index] = item; get_first_child(current),
prev,
if (is_animated) { prev.next,
to_animate.push(item); value,
} key,
} i,
} render_fn,
flags
// if we need to move items (as opposed to just adding/removing), );
// figure out how to do so efficiently (I would be lying if I said prev = item;
// I fully understand this part) current = item.next;
if (moved) {
mark_lis(sources);
} else if (is_controlled && to_destroy.length === a_items.length) {
// We can optimize the case in which all items are replaced —
// destroy everything first, then append new items
pause_effects(to_destroy, anchor);
to_destroy = [];
}
// working from the back, insert new or moved items
while (b-- > start) {
index = sources[b - start];
var should_insert = index === NEW_ITEM;
if (should_insert) {
if (last_item !== undefined) anchor = get_first_child(last_item);
item = create_item(anchor, array[b], keys[b], b, render_fn, flags);
} else {
item = b_items[b];
if (should_update) {
update_item(item, array[b], b, flags);
}
if (moved && index !== LIS_ITEM) {
if (last_item !== undefined) anchor = get_first_child(last_item);
move(/** @type {import('#client').Dom} */ (item.e.dom), anchor);
}
}
last_item = b_items[b] = item;
}
}
if (to_animate.length > 0) {
// TODO we need to briefly take any outroing elements out of the flow, so that
// we can figure out the eventual destination of the animating elements
// - https://github.com/sveltejs/svelte/pull/10798#issuecomment-2013681778
// - https://svelte.dev/repl/6e891305e9644a7ca7065fa95c79d2d2?version=4.2.9
effect(() => {
untrack(() => {
for (item of to_animate) {
item.a?.apply();
} }
});
});
} }
var controlled_anchor = is_controlled && b_items.length === 0 ? anchor : null;
pause_effects(to_destroy, controlled_anchor, () => {
state.items = b_items;
});
} }
/** /**
@ -512,6 +406,8 @@ function update_item(item, value, index, type) {
/** /**
* @template V * @template V
* @param {Node} anchor * @param {Node} anchor
* @param {import('#client').EachItem | import('#client').EachState | null} prev
* @param {import('#client').EachItem | null} next
* @param {V} value * @param {V} value
* @param {unknown} key * @param {unknown} key
* @param {number} index * @param {number} index
@ -519,7 +415,7 @@ function update_item(item, value, index, type) {
* @param {number} flags * @param {number} flags
* @returns {import('#client').EachItem} * @returns {import('#client').EachItem}
*/ */
function create_item(anchor, value, key, index, render_fn, flags) { function create_item(anchor, prev, next, value, key, index, render_fn, flags) {
var previous_each_item = current_each_item; var previous_each_item = current_each_item;
try { try {
@ -536,9 +432,14 @@ function create_item(anchor, value, key, index, render_fn, flags) {
k: key, k: key,
a: null, a: null,
// @ts-expect-error // @ts-expect-error
e: null e: null,
prev,
next
}; };
if (prev) prev.next = item;
if (next) next.prev = item;
current_each_item = item; current_each_item = item;
item.e = branch(() => render_fn(anchor, v, i)); item.e = branch(() => render_fn(anchor, v, i));

@ -50,8 +50,8 @@ export type Dom = TemplateNode | TemplateNode[];
export type EachState = { export type EachState = {
/** flags */ /** flags */
flags: number; flags: number;
/** items */ /** head of the linked list of items */
items: EachItem[]; next: EachItem | null;
}; };
export type EachItem = { export type EachItem = {
@ -65,6 +65,8 @@ export type EachItem = {
i: number | Source<number>; i: number | Source<number>;
/** key */ /** key */
k: unknown; k: unknown;
prev: EachItem | EachState | null;
next: EachItem | null;
}; };
export interface TransitionManager { export interface TransitionManager {

Loading…
Cancel
Save