From 54a17b57a0420574e32423f50c5c150d4d63c47f Mon Sep 17 00:00:00 2001 From: Rich Harris Date: Tue, 21 Jan 2025 06:50:21 -0500 Subject: [PATCH 1/3] only call onchange callbacks once per array mutation --- packages/svelte/src/internal/client/proxy.js | 21 +++++++++++++++++-- .../src/internal/client/reactivity/sources.js | 12 ++++++++++- 2 files changed, 30 insertions(+), 3 deletions(-) diff --git a/packages/svelte/src/internal/client/proxy.js b/packages/svelte/src/internal/client/proxy.js index 4acc52c943ec..05fc5240f427 100644 --- a/packages/svelte/src/internal/client/proxy.js +++ b/packages/svelte/src/internal/client/proxy.js @@ -9,13 +9,15 @@ import { object_prototype } from '../shared/utils.js'; import { check_ownership, widen_ownership } from './dev/ownership.js'; -import { source, set, state } from './reactivity/sources.js'; +import { source, set, state, set_call_onchange } from './reactivity/sources.js'; import { STATE_SYMBOL, STATE_SYMBOL_METADATA } from './constants.js'; import { UNINITIALIZED } from '../../constants.js'; import * as e from './errors.js'; import { get_stack } from './dev/tracing.js'; import { tracing_mode_flag } from '../flags/index.js'; +const array_methods = ['push', 'pop', 'shift', 'unshift', 'splice', 'reverse', 'sort']; + /** * @template T * @param {T} value @@ -168,7 +170,22 @@ export function proxy(value, options, parent = null, prev) { return v === UNINITIALIZED ? undefined : v; } - return Reflect.get(target, prop, receiver); + const value = Reflect.get(target, prop, receiver); + + if (is_proxied_array && array_methods.includes(/** @type {string} */ (prop))) { + // @ts-expect-error + return (...args) => { + set_call_onchange(false); + const result = value.apply(receiver, args); + set_call_onchange(true); + + options?.onchange?.(); + + return result; + }; + } + + return value; }, getOwnPropertyDescriptor(target, prop) { diff --git a/packages/svelte/src/internal/client/reactivity/sources.js b/packages/svelte/src/internal/client/reactivity/sources.js index 971bc1b336c4..1cfd65717c98 100644 --- a/packages/svelte/src/internal/client/reactivity/sources.js +++ b/packages/svelte/src/internal/client/reactivity/sources.js @@ -45,6 +45,13 @@ export function set_inspect_effects(v) { inspect_effects = v; } +let call_onchange = true; + +/** @param {boolean} v */ +export function set_call_onchange(v) { + call_onchange = v; +} + /** * @template V * @param {V} v @@ -191,7 +198,10 @@ export function internal_set(source, value) { var old_value = source.v; source.v = value; source.wv = increment_write_version(); - untrack(() => source.o?.onchange?.()); + + if (call_onchange) { + untrack(() => source.o?.onchange?.()); + } if (DEV && tracing_mode_flag) { source.updated = get_stack('UpdatedAt'); From e954b99afbcad02505a1ebf2cdff40e4a388e73b Mon Sep 17 00:00:00 2001 From: Rich Harris Date: Tue, 21 Jan 2025 07:05:14 -0500 Subject: [PATCH 2/3] fix --- packages/svelte/src/internal/client/proxy.js | 13 ++----- .../src/internal/client/reactivity/sources.js | 36 +++++++++++++++---- 2 files changed, 32 insertions(+), 17 deletions(-) diff --git a/packages/svelte/src/internal/client/proxy.js b/packages/svelte/src/internal/client/proxy.js index 05fc5240f427..860883a60d81 100644 --- a/packages/svelte/src/internal/client/proxy.js +++ b/packages/svelte/src/internal/client/proxy.js @@ -9,7 +9,7 @@ import { object_prototype } from '../shared/utils.js'; import { check_ownership, widen_ownership } from './dev/ownership.js'; -import { source, set, state, set_call_onchange } from './reactivity/sources.js'; +import { source, set, state, batch_onchange } from './reactivity/sources.js'; import { STATE_SYMBOL, STATE_SYMBOL_METADATA } from './constants.js'; import { UNINITIALIZED } from '../../constants.js'; import * as e from './errors.js'; @@ -173,16 +173,7 @@ export function proxy(value, options, parent = null, prev) { const value = Reflect.get(target, prop, receiver); if (is_proxied_array && array_methods.includes(/** @type {string} */ (prop))) { - // @ts-expect-error - return (...args) => { - set_call_onchange(false); - const result = value.apply(receiver, args); - set_call_onchange(true); - - options?.onchange?.(); - - return result; - }; + return batch_onchange(value); } return value; diff --git a/packages/svelte/src/internal/client/reactivity/sources.js b/packages/svelte/src/internal/client/reactivity/sources.js index 1cfd65717c98..405a19805015 100644 --- a/packages/svelte/src/internal/client/reactivity/sources.js +++ b/packages/svelte/src/internal/client/reactivity/sources.js @@ -45,11 +45,30 @@ export function set_inspect_effects(v) { inspect_effects = v; } -let call_onchange = true; +/** @type {null | Set<() => void>} */ +let onchange_batch = null; -/** @param {boolean} v */ -export function set_call_onchange(v) { - call_onchange = v; +/** + * @param {Function} fn + */ +export function batch_onchange(fn) { + // @ts-expect-error + return function (...args) { + let previous_onchange_batch = onchange_batch; + + try { + onchange_batch = new Set(); + + // @ts-expect-error + return fn.apply(this, args); + } finally { + for (const onchange of /** @type {Set<() => void>} */ (onchange_batch)) { + onchange(); + } + + onchange_batch = previous_onchange_batch; + } + }; } /** @@ -199,8 +218,13 @@ export function internal_set(source, value) { source.v = value; source.wv = increment_write_version(); - if (call_onchange) { - untrack(() => source.o?.onchange?.()); + var onchange = source.o?.onchange; + if (onchange) { + if (onchange_batch) { + onchange_batch.add(onchange); + } else { + onchange(); + } } if (DEV && tracing_mode_flag) { From de827fb0038aa6fafca18c33cc23390811c44e87 Mon Sep 17 00:00:00 2001 From: Rich Harris Date: Tue, 21 Jan 2025 07:09:44 -0500 Subject: [PATCH 3/3] fix --- packages/svelte/src/internal/client/proxy.js | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/packages/svelte/src/internal/client/proxy.js b/packages/svelte/src/internal/client/proxy.js index 860883a60d81..dcb6f46e8129 100644 --- a/packages/svelte/src/internal/client/proxy.js +++ b/packages/svelte/src/internal/client/proxy.js @@ -170,13 +170,13 @@ export function proxy(value, options, parent = null, prev) { return v === UNINITIALIZED ? undefined : v; } - const value = Reflect.get(target, prop, receiver); + v = Reflect.get(target, prop, receiver); if (is_proxied_array && array_methods.includes(/** @type {string} */ (prop))) { - return batch_onchange(value); + return batch_onchange(v); } - return value; + return v; }, getOwnPropertyDescriptor(target, prop) {