feat: use linked lists for each blocks (#11107)

* unify indexed/keyed each blocks

* WIP

* comment out console temporarily

* WIP

* animations

* fix animations

* mostly working

* working

* revert unnecessary test changes

* remove unused code

* noop when item matches

* add test

* DRY

* simplify

* mostly working

* fix

* fix

* uncomment

* remove unnecessary test

* unused

* appease eslint etc

* avoid mutating lookup

* reuse lookup

* perf tweaks

* microoptimisations

* more efficient linking

* optimise
pull/11111/head
Rich Harris 1 year ago committed by GitHub
parent 48202597de
commit 4d0b743918
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194

@ -2315,7 +2315,7 @@ export const template_visitors = {
) )
: /** @type {import('estree').Expression} */ (context.visit(node.key)) : /** @type {import('estree').Expression} */ (context.visit(node.key))
) )
: b.literal(null); : b.id('$.index');
if (node.index && each_node_meta.contains_group_binding) { if (node.index && each_node_meta.contains_group_binding) {
// We needed to create a unique identifier for the index above, but we want to use the // We needed to create a unique identifier for the index above, but we want to use the
@ -2323,36 +2323,21 @@ export const template_visitors = {
declarations.push(b.let(node.index, index)); declarations.push(b.let(node.index, index));
} }
let callee = '$.each_indexed'; if (context.state.options.dev && (each_type & EACH_KEYED) !== 0) {
context.state.init.push(
/** @type {import('estree').Expression[]} */ b.stmt(b.call('$.validate_each_keys', b.thunk(collection), key_function))
const args = [];
if ((each_type & EACH_KEYED) !== 0) {
if (context.state.options.dev && key_function.type !== 'Literal') {
context.state.init.push(
b.stmt(b.call('$.validate_each_keys', b.thunk(collection), key_function))
);
}
callee = '$.each_keyed';
args.push(
context.state.node,
b.literal(each_type),
each_node_meta.array_name ? each_node_meta.array_name : b.thunk(collection),
key_function,
b.arrow([b.id('$$anchor'), item, index], b.block(declarations.concat(children)))
);
} else {
args.push(
context.state.node,
b.literal(each_type),
each_node_meta.array_name ? each_node_meta.array_name : b.thunk(collection),
b.arrow([b.id('$$anchor'), item, index], b.block(declarations.concat(children)))
); );
} }
/** @type {import('estree').Expression[]} */
const args = [
context.state.node,
b.literal(each_type),
each_node_meta.array_name ? each_node_meta.array_name : b.thunk(collection),
key_function,
b.arrow([b.id('$$anchor'), item, index], b.block(declarations.concat(children)))
];
if (node.fallback) { if (node.fallback) {
args.push( args.push(
b.arrow( b.arrow(
@ -2362,7 +2347,7 @@ export const template_visitors = {
); );
} }
context.state.init.push(b.stmt(b.call(callee, ...args))); context.state.init.push(b.stmt(b.call('$.each', ...args)));
}, },
IfBlock(node, context) { IfBlock(node, context) {
context.state.template.push('<!>'); context.state.template.push('<!>');

@ -26,9 +26,6 @@ import { source, mutable_source, set } from '../../reactivity/sources.js';
import { is_array, is_frozen, map_get, map_set } from '../../utils.js'; import { is_array, is_frozen, map_get, map_set } from '../../utils.js';
import { STATE_SYMBOL } from '../../constants.js'; import { STATE_SYMBOL } from '../../constants.js';
var NEW_ITEM = -1;
var LIS_ITEM = -2;
/** /**
* The row of a keyed each block that is currently updating. We track this * The row of a keyed each block that is currently updating. We track this
* so that `animate:` directives have something to attach themselves to * so that `animate:` directives have something to attach themselves to
@ -41,25 +38,33 @@ export function set_current_each_item(item) {
current_each_item = item; current_each_item = item;
} }
/**
* @param {any} _
* @param {number} i
*/
export function index(_, i) {
return i;
}
/** /**
* Pause multiple effects simultaneously, and coordinate their * Pause multiple effects simultaneously, and coordinate their
* subsequent destruction. Used in each blocks * subsequent destruction. Used in each blocks
* @param {import('#client').Effect[]} effects * @param {import('#client').EachItem[]} items
* @param {null | Node} controlled_anchor * @param {null | Node} controlled_anchor
* @param {() => void} [callback] * @param {() => void} [callback]
*/ */
function pause_effects(effects, controlled_anchor, callback) { function pause_effects(items, controlled_anchor, callback) {
/** @type {import('#client').TransitionManager[]} */ /** @type {import('#client').TransitionManager[]} */
var transitions = []; var transitions = [];
var length = effects.length; var length = items.length;
for (var i = 0; i < length; i++) { for (var i = 0; i < length; i++) {
pause_children(effects[i], transitions, true); pause_children(items[i].e, transitions, true);
} }
// If we have a controlled anchor, it means that the each block is inside a single // If we have a controlled anchor, it means that the each block is inside a single
// DOM element, so we can apply a fast-path for clearing the contents of the element. // DOM element, so we can apply a fast-path for clearing the contents of the element.
if (effects.length > 0 && transitions.length === 0 && controlled_anchor !== null) { if (length > 0 && transitions.length === 0 && controlled_anchor !== null) {
var parent_node = /** @type {Element} */ (controlled_anchor.parentNode); var parent_node = /** @type {Element} */ (controlled_anchor.parentNode);
parent_node.textContent = ''; parent_node.textContent = '';
parent_node.append(controlled_anchor); parent_node.append(controlled_anchor);
@ -67,7 +72,7 @@ function pause_effects(effects, controlled_anchor, callback) {
run_out_transitions(transitions, () => { run_out_transitions(transitions, () => {
for (var i = 0; i < length; i++) { for (var i = 0; i < length; i++) {
destroy_effect(effects[i]); destroy_effect(items[i].e);
} }
if (callback !== undefined) callback(); if (callback !== undefined) callback();
@ -79,15 +84,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
* @param {typeof reconcile_indexed_array | reconcile_tracked_array} reconcile_fn
* @returns {void} * @returns {void}
*/ */
function each(anchor, flags, get_collection, get_key, render_fn, fallback_fn, reconcile_fn) { 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, items: new Map(), next: null };
var is_controlled = (flags & EACH_IS_CONTROLLED) !== 0; var is_controlled = (flags & EACH_IS_CONTROLLED) !== 0;
@ -113,8 +117,6 @@ function each(anchor, flags, get_collection, get_key, render_fn, fallback_fn, re
? [] ? []
: Array.from(collection); : Array.from(collection);
var keys = get_key === null ? array : array.map(get_key);
var length = array.length; var length = array.length;
// If we are working with an array that isn't proxied or frozen, then remove strict equality and ensure the items // If we are working with an array that isn't proxied or frozen, then remove strict equality and ensure the items
@ -145,11 +147,15 @@ function each(anchor, flags, get_collection, get_key, render_fn, fallback_fn, re
// 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;
/** @type {import('#client').EachItem} */
var item;
for (var i = 0; i < length; i++) { for (var i = 0; i < length; i++) {
if ( if (
child_anchor.nodeType !== 8 || child_anchor.nodeType !== 8 ||
@ -163,8 +169,13 @@ function each(anchor, flags, get_collection, get_key, render_fn, fallback_fn, re
} }
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 value = array[i];
var key = get_key(value, i);
item = create_item(child_anchor, prev, null, value, key, i, render_fn, flags);
state.items.set(key, item);
child_anchor = /** @type {Comment} */ (child_anchor.nextSibling); child_anchor = /** @type {Comment} */ (child_anchor.nextSibling);
prev = item;
} }
// remove excess nodes // remove excess nodes
@ -175,12 +186,10 @@ function each(anchor, flags, get_collection, get_key, render_fn, fallback_fn, re
child_anchor = next; child_anchor = next;
} }
} }
state.items = b_items;
} }
if (!hydrating) { if (!hydrating) {
reconcile_fn(array, state, anchor, render_fn, flags, keys); reconcile(array, state, anchor, render_fn, flags, get_key);
} }
if (fallback_fn !== null) { if (fallback_fn !== null) {
@ -204,33 +213,6 @@ function each(anchor, flags, get_collection, get_key, render_fn, fallback_fn, re
}); });
} }
/**
* @template V
* @param {Element | Comment} anchor
* @param {number} flags
* @param {() => V[]} get_collection
* @param {null | ((item: V) => string)} get_key
* @param {(anchor: Node, item: import('#client').MaybeSource<V>, index: import('#client').MaybeSource<number>) => void} render_fn
* @param {null | ((anchor: Node) => void)} [fallback_fn]
* @returns {void}
*/
export function each_keyed(anchor, flags, get_collection, get_key, render_fn, fallback_fn = null) {
each(anchor, flags, get_collection, get_key, render_fn, fallback_fn, reconcile_tracked_array);
}
/**
* @template V
* @param {Element | Comment} anchor
* @param {number} flags
* @param {() => V[]} get_collection
* @param {(anchor: Node, item: import('#client').MaybeSource<V>, index: import('#client').MaybeSource<number>) => void} render_fn
* @param {null | ((anchor: Node) => void)} [fallback_fn]
* @returns {void}
*/
export function each_indexed(anchor, flags, get_collection, render_fn, fallback_fn = null) {
each(anchor, flags, get_collection, null, render_fn, fallback_fn, reconcile_indexed_array);
}
/** /**
* @template V * @template V
* @param {Array<V>} array * @param {Array<V>} array
@ -238,308 +220,182 @@ export function each_indexed(anchor, flags, get_collection, 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 {(value: V, index: number) => any} get_key
* @returns {void} * @returns {void}
*/ */
function reconcile_indexed_array(array, state, anchor, render_fn, flags) { function reconcile(array, state, anchor, render_fn, flags, get_key) {
var a_items = state.items; var is_animated = (flags & EACH_IS_ANIMATED) !== 0;
var should_update = (flags & (EACH_ITEM_REACTIVE | EACH_INDEX_REACTIVE)) !== 0;
var a = a_items.length; var length = array.length;
var b = array.length; var items = state.items;
var min = Math.min(a, b); var first = state.next;
var current = first;
/** @type {typeof a_items} */ /** @type {Set<import('#client').EachItem>} */
var b_items = Array(b); var seen = new Set();
var item; /** @type {import('#client').EachState | import('#client').EachItem} */
var value; var prev = state;
// update items
for (var i = 0; i < min; i += 1) {
value = array[i];
item = a_items[i];
b_items[i] = item;
update_item(item, value, i, flags);
resume_effect(item.e);
}
if (b > a) { /** @type {import('#client').EachItem[]} */
// add items var to_animate = [];
for (; i < b; i += 1) {
value = array[i];
item = create_item(anchor, value, null, i, render_fn, flags);
b_items[i] = item;
}
state.items = b_items;
} else if (a > b) {
// remove items
var effects = [];
for (i = b; i < a; i += 1) {
effects.push(a_items[i].e);
}
var controlled_anchor = (flags & EACH_IS_CONTROLLED) !== 0 && b === 0 ? anchor : null;
pause_effects(effects, controlled_anchor, () => { /** @type {import('#client').EachItem[]} */
state.items.length = b; var matched = [];
});
}
}
/** /** @type {import('#client').EachItem[]} */
* Reconcile arrays by the equality of the elements in the array. This algorithm var stashed = [];
* is based on Ivi's reconcilation logic:
* https://github.com/localvoid/ivi/blob/9f1bd0918f487da5b131941228604763c5d8ef56/packages/ivi/src/client/core.ts#L968
* @template V
* @param {Array<V>} array
* @param {import('#client').EachState} state
* @param {Element | Comment | Text} anchor
* @param {(anchor: Node, item: import('#client').MaybeSource<V>, index: number | import('#client').Source<number>) => void} render_fn
* @param {number} flags
* @param {any[]} keys
* @returns {void}
*/
function reconcile_tracked_array(array, state, anchor, render_fn, flags, keys) {
var a_items = state.items;
var a = a_items.length; /** @type {V} */
var b = array.length; var value;
/** @type {Array<import('#client').EachItem>} */ /** @type {any} */
var b_items = Array(b); var key;
var is_animated = (flags & EACH_IS_ANIMATED) !== 0; /** @type {import('#client').EachItem | undefined} */
var should_update = (flags & (EACH_ITEM_REACTIVE | EACH_INDEX_REACTIVE)) !== 0;
var is_controlled = (flags & EACH_IS_CONTROLLED) !== 0;
var start = 0;
var item; var item;
/** @type {import('#client').Effect[]} */ /** @type {number} */
var to_destroy = []; var i;
/** @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 (is_animated) {
for (i = 0; i < length; i += 1) {
value = array[i];
key = get_key(value, i);
item = items.get(key);
if (should_update) { if (item !== undefined) {
update_item(item, array[b], b, flags); item.a?.measure();
} to_animate.push(item);
if (is_animated) { }
item.a?.measure();
to_animate.push(item);
} }
} }
// Step 2 — trim common prefix for (i = 0; i < length; i += 1) {
while (start < a && start < b && a_items[start].k === keys[start]) { value = array[i];
item = b_items[start] = a_items[start]; key = get_key(value, i);
item = items.get(key);
resume_effect(item.e);
if (item === undefined) {
prev = create_item(
current ? get_first_child(current) : anchor,
prev,
prev.next,
value,
key,
i,
render_fn,
flags
);
items.set(key, prev);
matched = [];
stashed = [];
current = prev.next;
continue;
}
if (should_update) { if (should_update) {
update_item(item, array[start], start, flags); update_item(item, value, i, flags);
} }
if (is_animated) {
item.a?.measure();
to_animate.push(item);
}
start += 1;
}
// Step 3 — update resume_effect(item.e);
if (start === a) {
// 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
for (i = start; i < b; i += 1) {
sources[i - start] = NEW_ITEM;
map_set(indexes, keys[i], i);
}
if (is_animated) { if (item !== current) {
// for all items that were in both the old and the new list, if (seen.has(item)) {
// measure them and store them in `to_animate` so we can if (matched.length < stashed.length) {
// apply animations once the DOM has been updated // more efficient to move later items to the front
for (i = 0; i < a_items.length; i += 1) { var start = stashed[0];
item = a_items[i]; var local_anchor = get_first_child(start);
if (indexes.has(item.k)) { var j;
item.a?.measure();
to_animate.push(item);
}
}
}
// populate the `sources` array for each old item with prev = start.prev;
// 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); var a = matched[0];
var b = matched[matched.length - 1];
if (index === undefined) { link(a.prev, b.next);
to_destroy.push(item.e); link(prev, a);
} else { link(b, start);
moved = true;
sources[index - start] = i;
b_items[index] = item;
if (is_animated) { for (j = 0; j < matched.length; j += 1) {
to_animate.push(item); move(matched[j], local_anchor);
} }
}
}
// if we need to move items (as opposed to just adding/removing), for (j = 0; j < stashed.length; j += 1) {
// figure out how to do so efficiently (I would be lying if I said seen.delete(stashed[j]);
// I fully understand this part) }
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 current = start;
while (b-- > start) { prev = b;
index = sources[b - start]; i -= 1;
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) { matched = [];
if (last_item !== undefined) anchor = get_first_child(last_item); stashed = [];
move(/** @type {import('#client').Dom} */ (item.e.dom), anchor); } else {
} // more efficient to move earlier items to the back
} seen.delete(item);
move(item, current ? get_first_child(current) : anchor);
last_item = b_items[b] = item; link(item.prev, item.next);
} link(item, prev.next);
} link(prev, item);
if (to_animate.length > 0) { prev = item;
// 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; continue;
}
pause_effects(to_destroy, controlled_anchor, () => { matched = [];
state.items = b_items; stashed = [];
});
}
/** while (current !== null && current.k !== key) {
* Longest Increased Subsequence algorithm seen.add(current);
* @param {Int32Array} a stashed.push(current);
* @returns {void} current = current.next;
*/ }
function mark_lis(a) {
var length = a.length;
var parent = new Int32Array(length);
var index = new Int32Array(length);
var index_length = 0;
var i = 0;
/** @type {number} */
var j;
/** @type {number} */
var k;
/** @type {number} */ if (current === null) {
var lo; continue;
}
/** @type {number} */ item = current;
var hi; }
// Skip -1 values at the start of the input array `a`. matched.push(item);
for (; a[i] === NEW_ITEM; ++i) { prev = item;
/**/ current = item.next;
} }
index[0] = i++; const to_destroy = Array.from(seen);
for (; i < length; ++i) {
k = a[i];
if (k !== NEW_ITEM) {
// Ignore -1 values.
j = index[index_length];
if (a[j] < k) { while (current) {
parent[i] = j; to_destroy.push(current);
index[++index_length] = i; current = current.next;
} else { }
lo = 0;
hi = index_length;
while (lo < hi) { var controlled_anchor = (flags & EACH_IS_CONTROLLED) !== 0 && length === 0 ? anchor : null;
j = (lo + hi) >> 1;
if (a[index[j]] < k) {
lo = j + 1;
} else {
hi = j;
}
}
if (k < a[index[lo]]) { pause_effects(to_destroy, controlled_anchor, () => {
if (lo > 0) { for (var i = 0; i < to_destroy.length; i += 1) {
parent[i] = index[lo - 1]; var item = to_destroy[i];
} items.delete(item.k);
index[lo] = i; link(item.prev, item.next);
}
}
} }
} });
// Mutate input array `a` and assign -2 value to all nodes that are part of LIS.
j = index[index_length];
while (index_length-- >= 0) { if (is_animated) {
a[j] = LIS_ITEM; effect(() => {
j = parent[j]; untrack(() => {
for (item of to_animate) {
item.a?.apply();
}
});
});
} }
} }
@ -579,6 +435,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} 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
@ -586,7 +444,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 {
@ -603,9 +461,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
}; };
prev.next = item;
if (next !== null) 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));
@ -616,15 +479,29 @@ function create_item(anchor, value, key, index, render_fn, flags) {
} }
/** /**
* @param {import('#client').Dom} current * @param {import('#client').EachItem} item
* @param {Text | Element | Comment} anchor * @param {Text | Element | Comment} anchor
*/ */
function move(current, anchor) { function move(item, anchor) {
if (is_array(current)) { var dom = item.e.dom;
for (var i = 0; i < current.length; i++) {
anchor.before(current[i]); if (dom !== null) {
if (is_array(dom)) {
for (var i = 0; i < dom.length; i++) {
anchor.before(dom[i]);
}
} else {
anchor.before(dom);
} }
} else {
anchor.before(current);
} }
} }
/**
*
* @param {import('#client').EachItem | import('#client').EachState} prev
* @param {import('#client').EachItem | null} next
*/
function link(prev, next) {
prev.next = next;
if (next !== null) next.prev = prev;
}

@ -98,14 +98,14 @@ export function animation(element, get_fn, get_params) {
to = this.element.getBoundingClientRect(); to = this.element.getBoundingClientRect();
const options = get_fn()(this.element, { from, to }, get_params?.());
if ( if (
from.left !== to.left || from.left !== to.left ||
from.right !== to.right || from.right !== to.right ||
from.top !== to.top || from.top !== to.top ||
from.bottom !== to.bottom from.bottom !== to.bottom
) { ) {
const options = get_fn()(this.element, { from, to }, get_params?.());
animation = animate(this.element, options, undefined, 1, () => { animation = animate(this.element, options, undefined, 1, () => {
animation?.abort(); animation?.abort();
animation = undefined; animation = undefined;

@ -3,7 +3,7 @@ export { await_block as await } from './dom/blocks/await.js';
export { if_block as if } from './dom/blocks/if.js'; export { if_block as if } from './dom/blocks/if.js';
export { key_block as key } from './dom/blocks/key.js'; export { key_block as key } from './dom/blocks/key.js';
export { css_props } from './dom/blocks/css-props.js'; export { css_props } from './dom/blocks/css-props.js';
export { each_keyed, each_indexed } from './dom/blocks/each.js'; export { index, each } from './dom/blocks/each.js';
export { html } from './dom/blocks/html.js'; export { html } from './dom/blocks/html.js';
export { snippet } from './dom/blocks/snippet.js'; export { snippet } from './dom/blocks/snippet.js';
export { component } from './dom/blocks/svelte-component.js'; export { component } from './dom/blocks/svelte-component.js';

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

@ -10,7 +10,7 @@ export default test({
intro: true, intro: true,
test({ assert, component, target, raf }) { test({ assert, component, target, raf }) {
let divs = /** @type {NodeListOf<HTMLDivElement & { foo: number, i: number }>} */ ( const divs = /** @type {NodeListOf<HTMLDivElement & { foo: number, i: number }>} */ (
target.querySelectorAll('div') target.querySelectorAll('div')
); );
divs[0].i = 0; // for debugging divs[0].i = 0; // for debugging
@ -43,20 +43,17 @@ export default test({
raf.tick(175); raf.tick(175);
assert.equal(divs[0].foo, 1); assert.equal(divs[0].foo, 1);
// Svelte 5, we no longer revert previous reconciled items assert.equal(divs[1].foo, 0.75);
// assert.equal(divs[1].foo, 0.75);
assert.equal(divs[2].foo, 1); assert.equal(divs[2].foo, 1);
raf.tick(225); raf.tick(225);
const divs3 = target.querySelectorAll('div'); const divs3 = target.querySelectorAll('div');
assert.strictEqual(divs[0], divs3[0]); assert.strictEqual(divs[0], divs3[0]);
// Svelte 5, we no longer revert previous reconciled items assert.strictEqual(divs[1], divs3[1]);
// assert.strictEqual(divs[1], divs3[1]);
assert.strictEqual(divs[2], divs3[2]); assert.strictEqual(divs[2], divs3[2]);
assert.equal(divs[0].foo, 1); assert.equal(divs[0].foo, 1);
// Svelte 5, we no longer revert previous reconciled items assert.equal(divs[1].foo, 1);
// assert.equal(divs[1].foo, 1);
assert.equal(divs[2].foo, 1); assert.equal(divs[2].foo, 1);
} }
}); });

@ -10,7 +10,7 @@ export default function Each_string_template($$anchor, $$props) {
var fragment = $.comment(); var fragment = $.comment();
var node = $.first_child(fragment); var node = $.first_child(fragment);
$.each_indexed(node, 1, () => ['foo', 'bar', 'baz'], ($$anchor, thing, $$index) => { $.each(node, 1, () => ['foo', 'bar', 'baz'], $.index, ($$anchor, thing, $$index) => {
var text = $.text($$anchor); var text = $.text($$anchor);
$.render_effect(() => $.set_text(text, `${$.stringify($.unwrap(thing))}, `)); $.render_effect(() => $.set_text(text, `${$.stringify($.unwrap(thing))}, `));

Loading…
Cancel
Save