diff --git a/.changeset/silver-points-approve.md b/.changeset/silver-points-approve.md new file mode 100644 index 0000000000..2946fc832c --- /dev/null +++ b/.changeset/silver-points-approve.md @@ -0,0 +1,5 @@ +--- +"svelte": patch +--- + +fix: improve indexed each array reconcilation diff --git a/packages/svelte/src/internal/client/each.js b/packages/svelte/src/internal/client/each.js index 8485f51958..68b5f446c2 100644 --- a/packages/svelte/src/internal/client/each.js +++ b/packages/svelte/src/internal/client/each.js @@ -14,14 +14,11 @@ import { set_current_hydration_fragment } from './hydration.js'; import { clear_text_content, map_get, map_set } from './operations.js'; -import { STATE_SYMBOL } from './proxy.js'; import { insert, remove } from './reconciler.js'; import { empty } from './render.js'; import { destroy_signal, execute_effect, - is_lazy_property, - lazy_property, mutable_source, push_destroy_fn, render_effect, @@ -132,7 +129,7 @@ function each(anchor_node, collection, flags, key_fn, render_fn, fallback_fn, re }; /** @param {import('./types.js').EachBlock} block */ - const clear_each = (block) => { + const render_each = (block) => { const flags = block.f; const is_controlled = (flags & EACH_IS_CONTROLLED) !== 0; const anchor_node = block.a; @@ -175,7 +172,7 @@ function each(anchor_node, collection, flags, key_fn, render_fn, fallback_fn, re if (fallback_fn !== null) { if (length === 0) { if (block.v.length !== 0 || render === null) { - clear_each(block); + render_each(block); create_fallback_effect(); return; } @@ -201,7 +198,7 @@ function each(anchor_node, collection, flags, key_fn, render_fn, fallback_fn, re false ); - render = render_effect(clear_each, block, true); + render = render_effect(render_each, block, true); if (mismatch) { // Set a fragment so that Svelte continues to operate in hydration mode @@ -279,14 +276,9 @@ function reconcile_indexed_array( flags, apply_transitions ) { - var is_proxied_array = STATE_SYMBOL in array && /** @type {any} */ (array[STATE_SYMBOL]).i; var a_blocks = each_block.v; var active_transitions = each_block.s; - if (is_proxied_array) { - flags &= ~EACH_ITEM_REACTIVE; - } - /** @type {number | void} */ var a = a_blocks.length; @@ -334,7 +326,7 @@ function reconcile_indexed_array( break; } - item = is_proxied_array ? lazy_property(array, index) : array[index]; + item = array[index]; block = each_item_block(item, null, index, render_fn, flags); b_blocks[index] = block; @@ -349,7 +341,7 @@ function reconcile_indexed_array( for (; index < length; index++) { if (index >= a) { // Add block - item = is_proxied_array ? lazy_property(array, index) : array[index]; + item = array[index]; block = each_item_block(item, null, index, render_fn, flags); b_blocks[index] = block; insert_each_item_block(block, dom, is_controlled, null); @@ -789,17 +781,6 @@ function update_each_item_block(block, item, index, type) { const block_v = block.v; if ((type & EACH_ITEM_REACTIVE) !== 0) { set_signal_value(block_v, item); - } else if (is_lazy_property(block_v)) { - // If we have lazy properties, it means that an array was used that has been - // proxied. Given this, we need to re-sync the old array by mutating the backing - // value to be the latest value to ensure the UI updates correctly. TODO: maybe - // we should bypass any internal mutation checks for this? - const o = block_v.o; - const p = block_v.p; - const prev = o[p]; - if (prev !== item) { - o[p] = item; - } } const transitions = block.s; const index_is_reactive = (type & EACH_INDEX_REACTIVE) !== 0; @@ -856,9 +837,7 @@ export function destroy_each_item_block( /** * @template V - * @template O - * @template P - * @param {V | import('./types.js').LazyProperty} item + * @param {V} item * @param {unknown} key * @param {number} index * @param {(anchor: null, item: V, index: number | import('./types.js').Signal) => void} render_fn diff --git a/packages/svelte/src/internal/client/runtime.js b/packages/svelte/src/internal/client/runtime.js index 6f6ac4742d..e200c6ec32 100644 --- a/packages/svelte/src/internal/client/runtime.js +++ b/packages/svelte/src/internal/client/runtime.js @@ -39,7 +39,6 @@ const FLUSH_MICROTASK = 0; const FLUSH_SYNC = 1; export const UNINITIALIZED = Symbol(); -export const LAZY_PROPERTY = Symbol(); // Used for controlling the flush of effects. let current_scheduler_mode = FLUSH_MICROTASK; @@ -1542,20 +1541,6 @@ export function is_signal(val) { ); } -/** - * @template O - * @template P - * @param {any} val - * @returns {val is import('./types.js').LazyProperty} - */ -export function is_lazy_property(val) { - return ( - typeof val === 'object' && - val !== null && - /** @type {import('./types.js').LazyProperty} */ (val).t === LAZY_PROPERTY - ); -} - /** * @template V * @param {unknown} val @@ -2084,21 +2069,6 @@ export function inspect(get_value, inspect = console.log) { }); } -/** - * @template O - * @template P - * @param {O} o - * @param {P} p - * @returns {import('./types.js').LazyProperty} - */ -export function lazy_property(o, p) { - return { - o, - p, - t: LAZY_PROPERTY - }; -} - /** * @template V * @param {V} value @@ -2109,9 +2079,6 @@ export function unwrap(value) { // @ts-ignore return get(value); } - if (is_lazy_property(value)) { - return value.o[value.p]; - } // @ts-ignore return value; } diff --git a/packages/svelte/src/internal/client/types.d.ts b/packages/svelte/src/internal/client/types.d.ts index c8918fa441..7d34789d3b 100644 --- a/packages/svelte/src/internal/client/types.d.ts +++ b/packages/svelte/src/internal/client/types.d.ts @@ -11,7 +11,7 @@ import { SNIPPET_BLOCK } from './block.js'; import type { READONLY_SYMBOL, STATE_SYMBOL } from './proxy.js'; -import { DERIVED, EFFECT, RENDER_EFFECT, SOURCE, PRE_EFFECT, LAZY_PROPERTY } from './runtime.js'; +import { DERIVED, EFFECT, RENDER_EFFECT, SOURCE, PRE_EFFECT } from './runtime.js'; // Put all internal types in this file. Once we convert to JSDoc, we can make this a d.ts file @@ -123,12 +123,6 @@ export type MaybeSignal = T | Signal; export type UnwrappedSignal = T extends Signal ? U : T; -export type LazyProperty = { - o: O; - p: P; - t: typeof LAZY_PROPERTY; -}; - export type EqualsFunctions = (a: T, v: T) => boolean; export type BlockType = diff --git a/packages/svelte/tests/runtime-runes/samples/each-updates-3/_config.js b/packages/svelte/tests/runtime-runes/samples/each-updates-3/_config.js new file mode 100644 index 0000000000..5caa453fed --- /dev/null +++ b/packages/svelte/tests/runtime-runes/samples/each-updates-3/_config.js @@ -0,0 +1,43 @@ +import { flushSync } from 'svelte'; +import { test } from '../../test'; + +export default test({ + html: `
  • \na\na
  • \nb\nb
  • \nc\nc
  • \nd\nd
`, + + async test({ assert, target }) { + /** + * @type {{ click: () => void; }} + */ + let btn1; + + [btn1] = target.querySelectorAll('button'); + + flushSync(() => { + btn1.click(); + }); + + assert.htmlEqual( + target.innerHTML, + `
  • \nb\nb
  • \nc\nc
  • \nd\nd
` + ); + + [btn1] = target.querySelectorAll('button'); + + flushSync(() => { + btn1.click(); + }); + + assert.htmlEqual( + target.innerHTML, + `
  • \nc\nc
  • \nd\nd
` + ); + + [btn1] = target.querySelectorAll('button'); + + flushSync(() => { + btn1.click(); + }); + + assert.htmlEqual(target.innerHTML, `
  • \nd\nd
`); + } +}); diff --git a/packages/svelte/tests/runtime-runes/samples/each-updates-3/main.svelte b/packages/svelte/tests/runtime-runes/samples/each-updates-3/main.svelte new file mode 100644 index 0000000000..35ab6cfc69 --- /dev/null +++ b/packages/svelte/tests/runtime-runes/samples/each-updates-3/main.svelte @@ -0,0 +1,36 @@ + + +
    + {#each entries as entry} +
  • + + {entry.id} + {#each entry.subitems as subitem} + {subitem} + {/each} +
  • + {/each} +