diff --git a/packages/svelte/src/internal/client/dom/blocks/each.js b/packages/svelte/src/internal/client/dom/blocks/each.js index 1c9adc60c0..b786382a83 100644 --- a/packages/svelte/src/internal/client/dom/blocks/each.js +++ b/packages/svelte/src/internal/client/dom/blocks/each.js @@ -24,7 +24,12 @@ import { push_destroy_fn, set_signal_value } from '../../runtime.js'; -import { pause_effect, render_effect, resume_effect } from '../../reactivity/computations.js'; +import { + destroy_effect, + pause_effect, + render_effect, + resume_effect +} from '../../reactivity/computations.js'; import { source, mutable_source } from '../../reactivity/sources.js'; import { trigger_transitions } from '../../transitions.js'; import { is_array } from '../../utils.js'; @@ -94,29 +99,19 @@ export function create_each_item_block(item, index, key) { } /** + * Reconcile arrays by the equality of the elements in the array. This algorithm + * is based on Ivi's reconcilation logic: + * https://github.com/localvoid/ivi/blob/9f1bd0918f487da5b131941228604763c5d8ef56/packages/ivi/src/client/core.ts#L968 * @template V * @param {Element | Comment} anchor_node * @param {() => V[]} collection * @param {number} flags - * @param {null | ((item: V) => string)} key_fn + * @param { ((item: V) => string)} key_fn * @param {(anchor: null, item: V, index: import('../../types.js').MaybeSignal) => void} render_fn * @param {null | ((anchor: Node) => void)} fallback_fn * @returns {void} */ export function each_keyed(anchor_node, collection, flags, key_fn, render_fn, fallback_fn) { - throw new Error('TODO each_keyed'); -} - -/** - * @template V - * @param {Element | Comment} anchor_node - * @param {() => V[]} collection - * @param {number} flags - * @param {(anchor: null, item: V, index: import('../../types.js').MaybeSignal) => void} render_fn - * @param {null | ((anchor: Node) => void)} fallback_fn - * @returns {void} - */ -export function each_indexed(anchor_node, collection, flags, render_fn, fallback_fn) { const is_controlled = (flags & EACH_IS_CONTROLLED) !== 0; hydrate_block_anchor(anchor_node, is_controlled); @@ -129,158 +124,43 @@ export function each_indexed(anchor_node, collection, flags, render_fn, fallback } } - /** - * Whether or not there was a "rendered fallback but want to render items" (or vice versa) hydration mismatch. - * Needs to be a `let` or else it isn't treeshaken out - */ - let mismatch = false; - - let length = 0; - - /** @type {Array} */ - let effects = []; - - /** @type {import('../../types.js').ComputationSignal | null} */ - let alternate; - - function truncate() { - let i = effects.length; - while (i--) { - if (effects[i]) { - effects.length = i + 1; - break; - } - } - } + /** @type {import('../../types.js').EachItemBlock[]} */ + var a_blocks = []; render_effect(() => { - const new_items = collection(); + /** @type {V[]} */ + const maybe_array = collection(); + var array = is_array(maybe_array) + ? maybe_array + : maybe_array == null + ? [] + : Array.from(maybe_array); - let nl = new_items ? new_items.length : 0; + const is_computed_key = true; - let hydrating_node = hydrating ? current_hydration_fragment[0] : null; + /** @type {number | void} */ + var a = a_blocks.length; - for (let i = length; i < nl; i += 1) { - if (effects[i]) { - resume_effect(effects[i]); - } else { - if (hydrating && !mismatch) { - let fragment = get_hydration_fragment(hydrating_node); - set_current_hydration_fragment(fragment); + /** @type {number} */ + var b = array.length; - if (!fragment) { - // If fragment is null, then that means that the server rendered less items than what - // the client code specifies -> break out and continue with client-side node creation - mismatch = true; - break; - } + /** @type {Array} */ + var b_blocks; - hydrating_node = fragment.at(-1).nextSibling?.nextSibling; - } - - effects[i] = render_effect( - () => - render_fn( - hydrating ? null : anchor_node, - () => { - return collection()[i]; - }, - i - ), - {}, - true - ); - } - } - - for (let i = nl; i < length; i += 1) { - const item = effects[i]; - if (item) { - pause_effect(item, () => { - effects[i] = null; - truncate(); - }); - } - } - - if (nl === 0) { - if (alternate) { - resume_effect(alternate); - } else if (fallback_fn) { - alternate = render_effect(() => fallback_fn(anchor_node), {}, true); - } - } else if (alternate) { - pause_effect(alternate, () => { - alternate = null; - }); - } - - length = nl; - }); -} - -/** - * Reconcile arrays by the equality of the elements in the array. This algorithm - * 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} array - * @param {import('../../types.js').EachBlock} each_block - * @param {Element | Comment | Text} dom - * @param {boolean} is_controlled - * @param {(anchor: null, item: V, index: number | import('../../types.js').Signal) => void} render_fn - * @param {number} flags - * @param {boolean} apply_transitions - * @param {Array | null} keys - * @returns {void} - */ -function reconcile_tracked_array( - array, - each_block, - dom, - is_controlled, - render_fn, - flags, - apply_transitions, - keys -) { - var a_blocks = each_block.v; - const is_computed_key = keys !== null; - var active_transitions = each_block.s; - - /** @type {number | void} */ - var a = a_blocks.length; - - /** @type {number} */ - var b = array.length; - - /** @type {Array} */ - var b_blocks; - var block; - - if (active_transitions.length !== 0) { - destroy_active_transition_blocks(active_transitions); - } - - if (b === 0) { - b_blocks = []; - // Remove old blocks - if (is_controlled && a !== 0) { - clear_text_content(dom); - } - while (a > 0) { - block = a_blocks[--a]; - destroy_each_item_block(block, active_transitions, apply_transitions, is_controlled); - } - } else { var a_end = a - 1; var b_end = b - 1; var key; var item; var idx; + /** `true` if there was a hydration mismatch. Needs to be a `let` or else it isn't treeshaken out */ let mismatch = false; + b_blocks = Array(b); + + var keys = array.map(key_fn); + var block; + if (hydrating) { // Hydrate block var fragment; @@ -322,7 +202,7 @@ function reconcile_tracked_array( key = is_computed_key ? keys[idx] : item; block = each_item_block(item, key, idx, render_fn, flags); b_blocks[idx] = block; - insert_each_item_block(block, dom, is_controlled, null); + insert_each_item_block(block, anchor_node, is_controlled, null); } } else { var is_animated = (flags & EACH_IS_ANIMATED) !== 0; @@ -334,6 +214,7 @@ function reconcile_tracked_array( var sibling = null; item = array[b_end]; key = is_computed_key ? keys[b_end] : item; + // Step 1 outer: while (true) { // From the end @@ -365,6 +246,7 @@ function reconcile_tracked_array( } break; } + // Step 2 if (start > a_end) { while (b_end >= start) { @@ -372,13 +254,14 @@ function reconcile_tracked_array( key = is_computed_key ? keys[b_end] : item; block = each_item_block(item, key, b_end, render_fn, flags); b_blocks[b_end--] = block; - sibling = insert_each_item_block(block, dom, is_controlled, sibling); + sibling = insert_each_item_block(block, anchor_node, is_controlled, sibling); } } else if (start > b_end) { b = start; do { if ((block = a_blocks[b++]) !== null) { - destroy_each_item_block(block, active_transitions, apply_transitions); + // destroy_each_item_block(block, [], false); + destroy_effect(block.e); } } while (b <= a_end); } else { @@ -394,6 +277,7 @@ function reconcile_tracked_array( key = is_computed_key ? keys[a] : item; map_set(item_index, key, a); } + // If keys are animated, we need to do updates before actual moves if (is_animated) { for (b = start; b <= a_end; ++b) { @@ -405,6 +289,7 @@ function reconcile_tracked_array( } } } + for (b = start; b <= a_end; ++b) { a = map_get(item_index, /** @type {V} */ (a_blocks[b].k)); block = a_blocks[b]; @@ -413,21 +298,26 @@ function reconcile_tracked_array( sources[a - start] = b; b_blocks[a] = block; } else if (block !== null) { - destroy_each_item_block(block, active_transitions, apply_transitions); + // destroy_each_item_block(block, [], false); + destroy_effect(block.e); } } + // Step 4 if (pos === MOVED_BLOCK) { mark_lis(sources); } + var last_block; var last_sibling; var should_create; + while (b_length-- > 0) { b_end = b_length + start; a = sources[b_length]; should_create = a === -1; item = array[b_end]; + if (should_create) { key = is_computed_key ? keys[b_end] : item; block = each_item_block(item, key, b_end, render_fn, flags); @@ -437,23 +327,137 @@ function reconcile_tracked_array( update_each_item_block(block, item, b_end, flags); } } + if (should_create || (pos === MOVED_BLOCK && a !== LIS_BLOCK)) { last_sibling = last_block === undefined ? sibling : get_first_child(last_block); - sibling = insert_each_item_block(block, dom, is_controlled, last_sibling); + sibling = insert_each_item_block(block, anchor_node, is_controlled, last_sibling); } + b_blocks[b_end] = block; last_block = block; } } + + if (mismatch) { + // Server rendered less nodes than the client -> set empty array so that Svelte continues to operate in hydration mode + set_current_hydration_fragment([]); + } } - if (mismatch) { - // Server rendered less nodes than the client -> set empty array so that Svelte continues to operate in hydration mode - set_current_hydration_fragment([]); + a_blocks = b_blocks; + }); +} + +/** + * @template V + * @param {Element | Comment} anchor_node + * @param {() => V[]} collection + * @param {number} flags + * @param {(anchor: null, item: V, index: import('../../types.js').MaybeSignal) => void} render_fn + * @param {null | ((anchor: Node) => void)} fallback_fn + * @returns {void} + */ +export function each_indexed(anchor_node, collection, flags, render_fn, fallback_fn) { + const is_controlled = (flags & EACH_IS_CONTROLLED) !== 0; + + hydrate_block_anchor(anchor_node, is_controlled); + + if (is_controlled) { + if (hydrating) { + anchor_node = /** @type {Comment} */ (anchor_node.firstChild); + } else { + anchor_node.appendChild((anchor_node = empty())); } } - each_block.v = b_blocks; + /** + * Whether or not there was a "rendered fallback but want to render items" (or vice versa) hydration mismatch. + * Needs to be a `let` or else it isn't treeshaken out + */ + let mismatch = false; + + let length = 0; + + /** @type {Array} */ + let effects = []; + + /** @type {import('../../types.js').ComputationSignal | null} */ + let alternate; + + function truncate() { + let i = effects.length; + while (i--) { + if (effects[i]) { + effects.length = i + 1; + break; + } + } + } + + render_effect(() => { + const new_items = collection(); + + let nl = new_items ? new_items.length : 0; + + let hydrating_node = hydrating ? current_hydration_fragment[0] : null; + + for (let i = length; i < nl; i += 1) { + if (effects[i]) { + resume_effect(effects[i]); + } else { + if (hydrating && !mismatch) { + let fragment = get_hydration_fragment(hydrating_node); + set_current_hydration_fragment(fragment); + + if (!fragment) { + // If fragment is null, then that means that the server rendered less items than what + // the client code specifies -> break out and continue with client-side node creation + mismatch = true; + break; + } + + hydrating_node = fragment.at(-1).nextSibling?.nextSibling; + } + + effects[i] = render_effect( + () => + render_fn( + hydrating ? null : anchor_node, + () => { + return collection()[i]; + }, + i + ), + {}, + true + ); + } + } + + for (let i = nl; i < length; i += 1) { + const item = effects[i]; + if (item) { + pause_effect(item, () => { + effects[i] = null; + truncate(); + }); + } + } + + if (nl === 0) { + if (alternate) { + resume_effect(alternate); + } else if (fallback_fn) { + alternate = render_effect(() => fallback_fn(anchor_node), {}, true); + } + } else if (alternate) { + pause_effect(alternate, () => { + alternate = null; + }); + } + + length = nl; + }); } /**