From 807ffbb6337dbbd03944a6041116ae9c5ca569fc Mon Sep 17 00:00:00 2001 From: Rich Harris Date: Tue, 21 Jan 2025 07:29:30 -0500 Subject: [PATCH] fix: only call `onchange` once for array mutations (#15073) * only call onchange callbacks once per array mutation * fix * fix --- packages/svelte/src/internal/client/proxy.js | 12 +++++-- .../src/internal/client/reactivity/sources.js | 36 ++++++++++++++++++- 2 files changed, 45 insertions(+), 3 deletions(-) diff --git a/packages/svelte/src/internal/client/proxy.js b/packages/svelte/src/internal/client/proxy.js index 4acc52c943..dcb6f46e81 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, 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'; 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,13 @@ export function proxy(value, options, parent = null, prev) { return v === UNINITIALIZED ? undefined : v; } - return Reflect.get(target, prop, receiver); + v = Reflect.get(target, prop, receiver); + + if (is_proxied_array && array_methods.includes(/** @type {string} */ (prop))) { + return batch_onchange(v); + } + + return v; }, getOwnPropertyDescriptor(target, prop) { diff --git a/packages/svelte/src/internal/client/reactivity/sources.js b/packages/svelte/src/internal/client/reactivity/sources.js index 971bc1b336..405a198050 100644 --- a/packages/svelte/src/internal/client/reactivity/sources.js +++ b/packages/svelte/src/internal/client/reactivity/sources.js @@ -45,6 +45,32 @@ export function set_inspect_effects(v) { inspect_effects = v; } +/** @type {null | Set<() => void>} */ +let onchange_batch = null; + +/** + * @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; + } + }; +} + /** * @template V * @param {V} v @@ -191,7 +217,15 @@ export function internal_set(source, value) { var old_value = source.v; source.v = value; source.wv = increment_write_version(); - 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) { source.updated = get_stack('UpdatedAt');