Skip to content

Commit 01933d5

Browse files
committed
fix: improve indexed each array reconcilation
1 parent f9f5d3a commit 01933d5

File tree

5 files changed

+89
-57
lines changed

5 files changed

+89
-57
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: 5 additions & 24 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;

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;
@@ -1566,20 +1565,6 @@ export function is_signal(val) {
15661565
);
15671566
}
15681567

1569-
/**
1570-
* @template O
1571-
* @template P
1572-
* @param {any} val
1573-
* @returns {val is import('./types.js').LazyProperty<O, P>}
1574-
*/
1575-
export function is_lazy_property(val) {
1576-
return (
1577-
typeof val === 'object' &&
1578-
val !== null &&
1579-
/** @type {import('./types.js').LazyProperty<O, P>} */ (val).t === LAZY_PROPERTY
1580-
);
1581-
}
1582-
15831568
/**
15841569
* @template V
15851570
* @param {unknown} val
@@ -2063,21 +2048,6 @@ export function inspect(get_value, inspect = console.log) {
20632048
});
20642049
}
20652050

2066-
/**
2067-
* @template O
2068-
* @template P
2069-
* @param {O} o
2070-
* @param {P} p
2071-
* @returns {import('./types.js').LazyProperty<O, P>}
2072-
*/
2073-
export function lazy_property(o, p) {
2074-
return {
2075-
o,
2076-
p,
2077-
t: LAZY_PROPERTY
2078-
};
2079-
}
2080-
20812051
/**
20822052
* @template V
20832053
* @param {V} value
@@ -2088,9 +2058,6 @@ export function unwrap(value) {
20882058
// @ts-ignore
20892059
return get(value);
20902060
}
2091-
if (is_lazy_property(value)) {
2092-
return value.o[value.p];
2093-
}
20942061
// @ts-ignore
20952062
return value;
20962063
}
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)