Skip to content

Commit 25e03b3

Browse files
committed
fix: batch assignment to length of an array
1 parent 3e886c7 commit 25e03b3

File tree

3 files changed

+84
-35
lines changed

3 files changed

+84
-35
lines changed

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

Lines changed: 52 additions & 35 deletions
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,15 @@ import { active_effect, get } from './runtime.js';
1919

2020
const array_methods = ['push', 'pop', 'shift', 'unshift', 'splice', 'reverse', 'sort'];
2121

22+
/**
23+
* Used to prevent batching in case we are not setting the length of an array
24+
* @param {any} fn
25+
* @returns
26+
*/
27+
function identity(fn) {
28+
return fn;
29+
}
30+
2231
/**
2332
* @param {ValueOptions | undefined} options
2433
* @returns {ValueOptions | undefined}
@@ -306,46 +315,54 @@ export function proxy(value, _options, parent = null, prev) {
306315
var s = sources.get(prop);
307316
var has = prop in target;
308317

309-
// variable.length = value -> clear all signals with index >= value
310-
if (is_proxied_array && prop === 'length') {
311-
for (var i = value; i < /** @type {Source<number>} */ (s).v; i += 1) {
312-
var other_s = sources.get(i + '');
313-
if (other_s !== undefined) {
314-
if (typeof other_s.v === 'object' && other_s.v !== null && STATE_SYMBOL in other_s.v) {
315-
other_s.v[PROXY_ONCHANGE_SYMBOL](options?.onchange, true);
318+
// if we are changing the length of the array we batch all the changes
319+
// to the sources and the original value by calling batch_onchange and immediately
320+
// invoking it...otherwise we just invoke an identity function
321+
(is_proxied_array && prop === 'length' ? batch_onchange : identity)(() => {
322+
// variable.length = value -> clear all signals with index >= value
323+
if (is_proxied_array && prop === 'length') {
324+
for (var i = value; i < /** @type {Source<number>} */ (s).v; i += 1) {
325+
var other_s = sources.get(i + '');
326+
if (other_s !== undefined) {
327+
if (
328+
typeof other_s.v === 'object' &&
329+
other_s.v !== null &&
330+
STATE_SYMBOL in other_s.v
331+
) {
332+
other_s.v[PROXY_ONCHANGE_SYMBOL](options?.onchange, true);
333+
}
334+
set(other_s, UNINITIALIZED);
335+
} else if (i in target) {
336+
// If the item exists in the original, we need to create a uninitialized source,
337+
// else a later read of the property would result in a source being created with
338+
// the value of the original item at that index.
339+
other_s = source(UNINITIALIZED, clone_options(options), stack);
340+
sources.set(i + '', other_s);
316341
}
317-
set(other_s, UNINITIALIZED);
318-
} else if (i in target) {
319-
// If the item exists in the original, we need to create a uninitialized source,
320-
// else a later read of the property would result in a source being created with
321-
// the value of the original item at that index.
322-
other_s = source(UNINITIALIZED, clone_options(options), stack);
323-
sources.set(i + '', other_s);
324342
}
325343
}
326-
}
327344

328-
// If we haven't yet created a source for this property, we need to ensure
329-
// we do so otherwise if we read it later, then the write won't be tracked and
330-
// the heuristics of effects will be different vs if we had read the proxied
331-
// object property before writing to that property.
332-
if (s === undefined) {
333-
if (!has || get_descriptor(target, prop)?.writable) {
334-
const opt = clone_options(options);
335-
s = source(undefined, opt, stack);
336-
set(s, proxy(value, opt, metadata));
337-
sources.set(prop, s);
338-
}
339-
} else {
340-
has = s.v !== UNINITIALIZED;
341-
// when we set a property if the source is a proxy we remove the current onchange from
342-
// the proxy `onchanges` so that it doesn't trigger it anymore
343-
if (typeof s.v === 'object' && s.v !== null && STATE_SYMBOL in s.v) {
344-
s.v[PROXY_ONCHANGE_SYMBOL](options?.onchange, true);
345+
// If we haven't yet created a source for this property, we need to ensure
346+
// we do so otherwise if we read it later, then the write won't be tracked and
347+
// the heuristics of effects will be different vs if we had read the proxied
348+
// object property before writing to that property.
349+
if (s === undefined) {
350+
if (!has || get_descriptor(target, prop)?.writable) {
351+
const opt = clone_options(options);
352+
s = source(undefined, opt, stack);
353+
set(s, proxy(value, opt, metadata));
354+
sources.set(prop, s);
355+
}
356+
} else {
357+
has = s.v !== UNINITIALIZED;
358+
// when we set a property if the source is a proxy we remove the current onchange from
359+
// the proxy `onchanges` so that it doesn't trigger it anymore
360+
if (typeof s.v === 'object' && s.v !== null && STATE_SYMBOL in s.v) {
361+
s.v[PROXY_ONCHANGE_SYMBOL](options?.onchange, true);
362+
}
363+
set(s, proxy(value, clone_options(options), metadata));
345364
}
346-
set(s, proxy(value, clone_options(options), metadata));
347-
}
348-
365+
})();
349366
if (DEV) {
350367
/** @type {ProxyMetadata | undefined} */
351368
var prop_metadata = value?.[STATE_SYMBOL_METADATA];
Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,18 @@
1+
import { flushSync } from 'svelte';
2+
import { test } from '../../test';
3+
4+
export default test({
5+
async test({ assert, target, logs }) {
6+
const [btn, btn2] = target.querySelectorAll('button');
7+
8+
flushSync(() => {
9+
btn.click();
10+
});
11+
assert.deepEqual(logs, [[{}, {}, {}, {}, {}, {}, {}, {}]]);
12+
13+
flushSync(() => {
14+
btn2.click();
15+
});
16+
assert.deepEqual(logs, [[{}, {}, {}, {}, {}, {}, {}, {}], []]);
17+
}
18+
});
Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,14 @@
1+
<script>
2+
let array = $state([], {
3+
onchange() {
4+
console.log($state.snapshot(array));
5+
}
6+
});
7+
</script>
8+
9+
<!-- clicking either of these buttons should result in at most one log -->
10+
<button onclick={() => array = [{}, {}, {}, {}, {}, {}, {}, {}]}>populate array</button>
11+
<button onclick={() => array.length = 0}>clear array</button>
12+
13+
<!-- without this, nested proxies aren't created -->
14+
<pre>{JSON.stringify(array)}</pre>

0 commit comments

Comments
 (0)