Skip to content

Commit 0e011ad

Browse files
authored
fix: improve indexed each array reconcilation (#10422)
* fix: improve indexed each array reconcilation * simplify
1 parent 623340a commit 0e011ad

File tree

6 files changed

+91
-67
lines changed

6 files changed

+91
-67
lines changed

.changeset/silver-points-approve.md

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
---
2+
"svelte": patch
3+
---
4+
5+
fix: improve indexed each array reconcilation

packages/svelte/src/internal/client/each.js

Lines changed: 6 additions & 27 deletions
Original file line numberDiff line numberDiff line change
@@ -14,14 +14,11 @@ import {
1414
set_current_hydration_fragment
1515
} from './hydration.js';
1616
import { clear_text_content, map_get, map_set } from './operations.js';
17-
import { STATE_SYMBOL } from './proxy.js';
1817
import { insert, remove } from './reconciler.js';
1918
import { empty } from './render.js';
2019
import {
2120
destroy_signal,
2221
execute_effect,
23-
is_lazy_property,
24-
lazy_property,
2522
mutable_source,
2623
push_destroy_fn,
2724
render_effect,
@@ -132,7 +129,7 @@ function each(anchor_node, collection, flags, key_fn, render_fn, fallback_fn, re
132129
};
133130

134131
/** @param {import('./types.js').EachBlock} block */
135-
const clear_each = (block) => {
132+
const render_each = (block) => {
136133
const flags = block.f;
137134
const is_controlled = (flags & EACH_IS_CONTROLLED) !== 0;
138135
const anchor_node = block.a;
@@ -175,7 +172,7 @@ function each(anchor_node, collection, flags, key_fn, render_fn, fallback_fn, re
175172
if (fallback_fn !== null) {
176173
if (length === 0) {
177174
if (block.v.length !== 0 || render === null) {
178-
clear_each(block);
175+
render_each(block);
179176
create_fallback_effect();
180177
return;
181178
}
@@ -201,7 +198,7 @@ function each(anchor_node, collection, flags, key_fn, render_fn, fallback_fn, re
201198
false
202199
);
203200

204-
render = render_effect(clear_each, block, true);
201+
render = render_effect(render_each, block, true);
205202

206203
if (mismatch) {
207204
// Set a fragment so that Svelte continues to operate in hydration mode
@@ -279,14 +276,9 @@ function reconcile_indexed_array(
279276
flags,
280277
apply_transitions
281278
) {
282-
var is_proxied_array = STATE_SYMBOL in array && /** @type {any} */ (array[STATE_SYMBOL]).i;
283279
var a_blocks = each_block.v;
284280
var active_transitions = each_block.s;
285281

286-
if (is_proxied_array) {
287-
flags &= ~EACH_ITEM_REACTIVE;
288-
}
289-
290282
/** @type {number | void} */
291283
var a = a_blocks.length;
292284

@@ -334,7 +326,7 @@ function reconcile_indexed_array(
334326
break;
335327
}
336328

337-
item = is_proxied_array ? lazy_property(array, index) : array[index];
329+
item = array[index];
338330
block = each_item_block(item, null, index, render_fn, flags);
339331
b_blocks[index] = block;
340332

@@ -349,7 +341,7 @@ function reconcile_indexed_array(
349341
for (; index < length; index++) {
350342
if (index >= a) {
351343
// Add block
352-
item = is_proxied_array ? lazy_property(array, index) : array[index];
344+
item = array[index];
353345
block = each_item_block(item, null, index, render_fn, flags);
354346
b_blocks[index] = block;
355347
insert_each_item_block(block, dom, is_controlled, null);
@@ -789,17 +781,6 @@ function update_each_item_block(block, item, index, type) {
789781
const block_v = block.v;
790782
if ((type & EACH_ITEM_REACTIVE) !== 0) {
791783
set_signal_value(block_v, item);
792-
} else if (is_lazy_property(block_v)) {
793-
// If we have lazy properties, it means that an array was used that has been
794-
// proxied. Given this, we need to re-sync the old array by mutating the backing
795-
// value to be the latest value to ensure the UI updates correctly. TODO: maybe
796-
// we should bypass any internal mutation checks for this?
797-
const o = block_v.o;
798-
const p = block_v.p;
799-
const prev = o[p];
800-
if (prev !== item) {
801-
o[p] = item;
802-
}
803784
}
804785
const transitions = block.s;
805786
const index_is_reactive = (type & EACH_INDEX_REACTIVE) !== 0;
@@ -856,9 +837,7 @@ export function destroy_each_item_block(
856837

857838
/**
858839
* @template V
859-
* @template O
860-
* @template P
861-
* @param {V | import('./types.js').LazyProperty<O, P>} item
840+
* @param {V} item
862841
* @param {unknown} key
863842
* @param {number} index
864843
* @param {(anchor: null, item: V, index: number | import('./types.js').Signal<number>) => void} render_fn

packages/svelte/src/internal/client/runtime.js

Lines changed: 0 additions & 33 deletions
Original file line numberDiff line numberDiff line change
@@ -39,7 +39,6 @@ const FLUSH_MICROTASK = 0;
3939
const FLUSH_SYNC = 1;
4040

4141
export const UNINITIALIZED = Symbol();
42-
export const LAZY_PROPERTY = Symbol();
4342

4443
// Used for controlling the flush of effects.
4544
let current_scheduler_mode = FLUSH_MICROTASK;
@@ -1542,20 +1541,6 @@ export function is_signal(val) {
15421541
);
15431542
}
15441543

1545-
/**
1546-
* @template O
1547-
* @template P
1548-
* @param {any} val
1549-
* @returns {val is import('./types.js').LazyProperty<O, P>}
1550-
*/
1551-
export function is_lazy_property(val) {
1552-
return (
1553-
typeof val === 'object' &&
1554-
val !== null &&
1555-
/** @type {import('./types.js').LazyProperty<O, P>} */ (val).t === LAZY_PROPERTY
1556-
);
1557-
}
1558-
15591544
/**
15601545
* @template V
15611546
* @param {unknown} val
@@ -2084,21 +2069,6 @@ export function inspect(get_value, inspect = console.log) {
20842069
});
20852070
}
20862071

2087-
/**
2088-
* @template O
2089-
* @template P
2090-
* @param {O} o
2091-
* @param {P} p
2092-
* @returns {import('./types.js').LazyProperty<O, P>}
2093-
*/
2094-
export function lazy_property(o, p) {
2095-
return {
2096-
o,
2097-
p,
2098-
t: LAZY_PROPERTY
2099-
};
2100-
}
2101-
21022072
/**
21032073
* @template V
21042074
* @param {V} value
@@ -2109,9 +2079,6 @@ export function unwrap(value) {
21092079
// @ts-ignore
21102080
return get(value);
21112081
}
2112-
if (is_lazy_property(value)) {
2113-
return value.o[value.p];
2114-
}
21152082
// @ts-ignore
21162083
return value;
21172084
}

packages/svelte/src/internal/client/types.d.ts

Lines changed: 1 addition & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -11,7 +11,7 @@ import {
1111
SNIPPET_BLOCK
1212
} from './block.js';
1313
import type { READONLY_SYMBOL, STATE_SYMBOL } from './proxy.js';
14-
import { DERIVED, EFFECT, RENDER_EFFECT, SOURCE, PRE_EFFECT, LAZY_PROPERTY } from './runtime.js';
14+
import { DERIVED, EFFECT, RENDER_EFFECT, SOURCE, PRE_EFFECT } from './runtime.js';
1515

1616
// Put all internal types in this file. Once we convert to JSDoc, we can make this a d.ts file
1717

@@ -123,12 +123,6 @@ export type MaybeSignal<T = unknown> = T | Signal<T>;
123123

124124
export type UnwrappedSignal<T> = T extends Signal<infer U> ? U : T;
125125

126-
export type LazyProperty<O, P> = {
127-
o: O;
128-
p: P;
129-
t: typeof LAZY_PROPERTY;
130-
};
131-
132126
export type EqualsFunctions<T = any> = (a: T, v: T) => boolean;
133127

134128
export type BlockType =
Lines changed: 43 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,43 @@
1+
import { flushSync } from 'svelte';
2+
import { test } from '../../test';
3+
4+
export default test({
5+
html: `<ul><li><button>Delete</button>\na\na</li><li><button>Delete</button>\nb\nb</li><li><button>Delete</button>\nc\nc</li><li><button>Delete</button>\nd\nd</li></ul>`,
6+
7+
async test({ assert, target }) {
8+
/**
9+
* @type {{ click: () => void; }}
10+
*/
11+
let btn1;
12+
13+
[btn1] = target.querySelectorAll('button');
14+
15+
flushSync(() => {
16+
btn1.click();
17+
});
18+
19+
assert.htmlEqual(
20+
target.innerHTML,
21+
`<ul><li><button>Delete</button>\nb\nb</li><li><button>Delete</button>\nc\nc</li><li><button>Delete</button>\nd\nd</li></ul>`
22+
);
23+
24+
[btn1] = target.querySelectorAll('button');
25+
26+
flushSync(() => {
27+
btn1.click();
28+
});
29+
30+
assert.htmlEqual(
31+
target.innerHTML,
32+
`<ul><li><button>Delete</button>\nc\nc</li><li><button>Delete</button>\nd\nd</li></ul>`
33+
);
34+
35+
[btn1] = target.querySelectorAll('button');
36+
37+
flushSync(() => {
38+
btn1.click();
39+
});
40+
41+
assert.htmlEqual(target.innerHTML, `<ul><li><button>Delete</button>\nd\nd</li></ul>`);
42+
}
43+
});
Lines changed: 36 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,36 @@
1+
<script>
2+
let entries = $state([
3+
{
4+
id: 'a',
5+
subitems: ['a'],
6+
},
7+
{
8+
id: 'b',
9+
subitems: ['b'],
10+
},
11+
{
12+
id: 'c',
13+
subitems: ['c'],
14+
},
15+
{
16+
id: 'd',
17+
subitems: ['d'],
18+
},
19+
]);
20+
21+
function onDeleteEntry(entry) {
22+
entries = entries.filter((innerEntry) => innerEntry.id !== entry.id);
23+
}
24+
</script>
25+
26+
<ul>
27+
{#each entries as entry}
28+
<li>
29+
<button on:click={() => onDeleteEntry(entry)}>Delete</button>
30+
{entry.id}
31+
{#each entry.subitems as subitem}
32+
{subitem}
33+
{/each}
34+
</li>
35+
{/each}
36+
</ul>

0 commit comments

Comments
 (0)