diff --git a/packages/svelte/src/compiler/phases/3-transform/client/visitors/CallExpression.js b/packages/svelte/src/compiler/phases/3-transform/client/visitors/CallExpression.js index 3e2f1414e6..0ddca0c148 100644 --- a/packages/svelte/src/compiler/phases/3-transform/client/visitors/CallExpression.js +++ b/packages/svelte/src/compiler/phases/3-transform/client/visitors/CallExpression.js @@ -5,6 +5,7 @@ import * as b from '#compiler/builders'; import { get_rune } from '../../../scope.js'; import { transform_inspect_rune } from '../../utils.js'; import { should_proxy } from '../utils.js'; +import { get_onchange } from './shared/state.js'; /** * @param {CallExpression} node @@ -24,6 +25,7 @@ export function CallExpression(node, context) { case '$state': case '$state.raw': { let arg = node.arguments[0]; + let onchange = get_onchange(/** @type {Expression} */ (node.arguments[1]), context); /** @type {Expression | undefined} */ let value = undefined; @@ -35,11 +37,11 @@ export function CallExpression(node, context) { rune === '$state' && should_proxy(/** @type {Expression} */ (arg), context.state.scope) ) { - value = b.call('$.proxy', value); + return b.call('$.assignable_proxy', value, onchange); } } - return b.call('$.state', value); + return b.call('$.state', value, onchange); } case '$derived': diff --git a/packages/svelte/src/compiler/phases/3-transform/client/visitors/VariableDeclaration.js b/packages/svelte/src/compiler/phases/3-transform/client/visitors/VariableDeclaration.js index 42d6d482e7..d1390cbae9 100644 --- a/packages/svelte/src/compiler/phases/3-transform/client/visitors/VariableDeclaration.js +++ b/packages/svelte/src/compiler/phases/3-transform/client/visitors/VariableDeclaration.js @@ -140,7 +140,7 @@ export function VariableDeclaration(node, context) { const is_proxy = should_proxy(value, context.state.scope); if (rune === '$state' && is_proxy) { - value = b.call('$.proxy', value, onchange); + value = b.call(is_state ? '$.assignable_proxy' : '$.proxy', value, onchange); if (dev && !is_state) { value = b.call('$.tag_proxy', value, b.literal(id.name)); @@ -148,7 +148,9 @@ export function VariableDeclaration(node, context) { } if (is_state) { - value = b.call('$.state', value, onchange); + if (!(rune === '$state' && is_proxy)) { + value = b.call('$.state', value, onchange); + } if (dev) { value = b.call('$.tag', value, b.literal(id.name)); diff --git a/packages/svelte/src/compiler/phases/3-transform/client/visitors/shared/state.js b/packages/svelte/src/compiler/phases/3-transform/client/visitors/shared/state.js index 0f8a7b1b5b..cba0789ee8 100644 --- a/packages/svelte/src/compiler/phases/3-transform/client/visitors/shared/state.js +++ b/packages/svelte/src/compiler/phases/3-transform/client/visitors/shared/state.js @@ -1,11 +1,11 @@ /** @import { Expression, Property } from 'estree' */ -/** @import { ComponentContext } from '../../types' */ +/** @import { ComponentContext, Context } from '../../types' */ import * as b from '../../../../../utils/builders.js'; /** * Extract the `onchange` callback from the options passed to `$state` * @param {Expression} options - * @param {ComponentContext} context + * @param {ComponentContext | Context} context * @returns {Expression | undefined} */ export function get_onchange(options, context) { diff --git a/packages/svelte/src/internal/client/proxy.js b/packages/svelte/src/internal/client/proxy.js index a14f273983..a2ed8ca50f 100644 --- a/packages/svelte/src/internal/client/proxy.js +++ b/packages/svelte/src/internal/client/proxy.js @@ -22,7 +22,8 @@ import { flush_inspect_effects, set_inspect_effects_deferred, batch_onchange, - state + state, + onchange_batch } from './reactivity/sources.js'; import { PROXY_PATH_SYMBOL, STATE_SYMBOL, PROXY_ONCHANGE_SYMBOL } from '#client/constants'; import { UNINITIALIZED } from '../../constants.js'; @@ -325,7 +326,7 @@ export function proxy(value, onchange) { // if we are changing the length of the array we batch all the changes // to the sources and the original value by calling batch_onchange and immediately // invoking it...otherwise we just invoke an identity function - (is_proxied_array && prop === 'length' ? batch_onchange : identity)(() => { + (is_proxied_array && prop === 'length' && !onchange_batch ? batch_onchange : identity)(() => { // variable.length = value -> clear all signals with index >= value if (is_proxied_array && prop === 'length') { for (var i = value; i < /** @type {Source} */ (s).v; i += 1) { @@ -361,9 +362,8 @@ export function proxy(value, onchange) { if (s === undefined) { if (!has || get_descriptor(target, prop)?.writable) { s = with_parent(() => source(undefined, onchange, stack)); - set(s, proxy(value, onchange)); - sources.set(prop, s); + set(s, proxy(value, onchange)); if (DEV) { tag(s, get_label(path, prop)); @@ -373,13 +373,13 @@ export function proxy(value, onchange) { has = s.v !== UNINITIALIZED; var p = with_parent(() => proxy(value, onchange)); - set(s, p); - // when we set a property if the source is a proxy we remove the current onchange from // the proxy `onchanges` so that it doesn't trigger it anymore if (onchange && typeof s.v === 'object' && s.v !== null && STATE_SYMBOL in s.v) { s.v[PROXY_ONCHANGE_SYMBOL](onchange, true); } + + set(s, p); } })(); diff --git a/packages/svelte/src/internal/client/reactivity/sources.js b/packages/svelte/src/internal/client/reactivity/sources.js index 3b88aed924..2b18d3a285 100644 --- a/packages/svelte/src/internal/client/reactivity/sources.js +++ b/packages/svelte/src/internal/client/reactivity/sources.js @@ -57,7 +57,7 @@ export function set_inspect_effects_deferred() { } /** @type {null | Set<() => void>} */ -let onchange_batch = null; +export let onchange_batch = null; /** * @param {Function} fn @@ -271,6 +271,15 @@ export function internal_set(source, value) { if (DEV && inspect_effects.size > 0 && !inspect_effects_deferred) { flush_inspect_effects(); } + + var onchange = source.o; + if (onchange) { + if (onchange_batch) { + onchange_batch.add(onchange); + } else { + onchange(); + } + } } return value; @@ -294,14 +303,6 @@ export function flush_inspect_effects() { } inspect_effects.clear(); - var onchange = source.o; - if (onchange) { - if (onchange_batch) { - onchange_batch.add(onchange); - } else { - onchange(); - } - } } /** diff --git a/packages/svelte/tests/runtime-runes/samples/state-onchange-classes/_config.js b/packages/svelte/tests/runtime-runes/samples/state-onchange-classes/_config.js index dc9fe8bfb1..9ed80bd660 100644 --- a/packages/svelte/tests/runtime-runes/samples/state-onchange-classes/_config.js +++ b/packages/svelte/tests/runtime-runes/samples/state-onchange-classes/_config.js @@ -3,9 +3,14 @@ import { test } from '../../test'; export default test({ async test({ assert, target, logs }) { - const [btn, btn2, btn3] = target.querySelectorAll('button'); + const [btn, btn2, btn3, btn4, btn5, btn6, btn7] = target.querySelectorAll('button'); - assert.deepEqual(logs, ['constructor count', 'constructor proxy']); + assert.deepEqual(logs, [ + 'constructor count', + 'constructor proxy', + 'assign in constructor', + 'assign in constructor proxy' + ]); logs.length = 0; @@ -17,5 +22,43 @@ export default test({ flushSync(() => btn3.click()); assert.deepEqual(logs, ['class count', 'class proxy', 'class proxy']); + + flushSync(() => btn4.click()); + assert.deepEqual(logs, [ + 'class count', + 'class proxy', + 'class proxy', + 'declared in constructor' + ]); + + flushSync(() => btn5.click()); + assert.deepEqual(logs, [ + 'class count', + 'class proxy', + 'class proxy', + 'declared in constructor', + 'declared in constructor' + ]); + + flushSync(() => btn6.click()); + assert.deepEqual(logs, [ + 'class count', + 'class proxy', + 'class proxy', + 'declared in constructor', + 'declared in constructor', + 'declared in constructor proxy' + ]); + + flushSync(() => btn7.click()); + assert.deepEqual(logs, [ + 'class count', + 'class proxy', + 'class proxy', + 'declared in constructor', + 'declared in constructor', + 'declared in constructor proxy', + 'declared in constructor proxy' + ]); } }); diff --git a/packages/svelte/tests/runtime-runes/samples/state-onchange-classes/main.svelte b/packages/svelte/tests/runtime-runes/samples/state-onchange-classes/main.svelte index a42c1c1245..8f49d5294b 100644 --- a/packages/svelte/tests/runtime-runes/samples/state-onchange-classes/main.svelte +++ b/packages/svelte/tests/runtime-runes/samples/state-onchange-classes/main.svelte @@ -23,9 +23,36 @@ } }); + declared_in_constructor; + declared_in_constructor_proxy; + #assign_in_constructor; + #assign_in_constructor_proxy; + constructor(){ this.#in_constructor = 42; this.#in_constructor_proxy.count++; + this.declared_in_constructor = $state(0, { + onchange(){ + console.log("declared in constructor"); + } + }); + this.declared_in_constructor_proxy = $state({ count: 0 }, { + onchange(){ + console.log("declared in constructor proxy"); + } + }); + this.#assign_in_constructor = $state(0, { + onchange(){ + console.log("assign in constructor"); + } + }); + this.#assign_in_constructor++; + this.#assign_in_constructor_proxy = $state({ count: 0 }, { + onchange(){ + console.log("assign in constructor proxy"); + } + }); + this.#assign_in_constructor_proxy.count++; } } @@ -34,4 +61,8 @@ - \ No newline at end of file + + + + + \ No newline at end of file diff --git a/packages/svelte/tests/runtime-runes/samples/state-raw-onchange/_config.js b/packages/svelte/tests/runtime-runes/samples/state-raw-onchange/_config.js index ce41a485c5..160da47283 100644 --- a/packages/svelte/tests/runtime-runes/samples/state-raw-onchange/_config.js +++ b/packages/svelte/tests/runtime-runes/samples/state-raw-onchange/_config.js @@ -3,10 +3,15 @@ import { test } from '../../test'; export default test({ async test({ assert, target, logs }) { - const [btn, btn2, btn3, btn4, btn5, btn6, btn7, btn8, btn9, btn10] = + const [btn, btn2, btn3, btn4, btn5, btn6, btn7, btn8, btn9, btn10, btn11, btn12, btn13] = target.querySelectorAll('button'); - assert.deepEqual(logs, ['constructor count', 'constructor object']); + assert.deepEqual(logs, [ + 'constructor count', + 'constructor object', + 'assign in constructor', + 'assign in constructor object' + ]); logs.length = 0; @@ -29,15 +34,73 @@ export default test({ assert.deepEqual(logs, ['count', 'object', 'class count', 'class object']); flushSync(() => btn7.click()); - assert.deepEqual(logs, ['count', 'object', 'class count', 'class object']); + assert.deepEqual(logs, [ + 'count', + 'object', + 'class count', + 'class object', + 'declared in constructor' + ]); flushSync(() => btn8.click()); - assert.deepEqual(logs, ['count', 'object', 'class count', 'class object']); + assert.deepEqual(logs, [ + 'count', + 'object', + 'class count', + 'class object', + 'declared in constructor', + 'declared in constructor object' + ]); flushSync(() => btn9.click()); - assert.deepEqual(logs, ['count', 'object', 'class count', 'class object']); + assert.deepEqual(logs, [ + 'count', + 'object', + 'class count', + 'class object', + 'declared in constructor', + 'declared in constructor object' + ]); flushSync(() => btn10.click()); - assert.deepEqual(logs, ['count', 'object', 'class count', 'class object', 'arr']); + assert.deepEqual(logs, [ + 'count', + 'object', + 'class count', + 'class object', + 'declared in constructor', + 'declared in constructor object' + ]); + + flushSync(() => btn11.click()); + assert.deepEqual(logs, [ + 'count', + 'object', + 'class count', + 'class object', + 'declared in constructor', + 'declared in constructor object' + ]); + + flushSync(() => btn12.click()); + assert.deepEqual(logs, [ + 'count', + 'object', + 'class count', + 'class object', + 'declared in constructor', + 'declared in constructor object' + ]); + + flushSync(() => btn13.click()); + assert.deepEqual(logs, [ + 'count', + 'object', + 'class count', + 'class object', + 'declared in constructor', + 'declared in constructor object', + 'arr' + ]); } }); diff --git a/packages/svelte/tests/runtime-runes/samples/state-raw-onchange/main.svelte b/packages/svelte/tests/runtime-runes/samples/state-raw-onchange/main.svelte index cc19a21d4f..2fc9f4e187 100644 --- a/packages/svelte/tests/runtime-runes/samples/state-raw-onchange/main.svelte +++ b/packages/svelte/tests/runtime-runes/samples/state-raw-onchange/main.svelte @@ -23,22 +23,48 @@ } }) - #in_constructor = $state(0, { + #in_constructor = $state.raw(0, { onchange(){ console.log("constructor count"); } }); - #in_constructor_proxy = $state({ count: 0 }, { + #in_constructor_obj = $state.raw({ count: 0 }, { onchange(){ console.log("constructor object"); } }); + declared_in_constructor; + declared_in_constructor_obj; + #assign_in_constructor; + #assign_in_constructor_obj; constructor(){ this.#in_constructor++; - this.#in_constructor_proxy.count++; + this.#in_constructor_obj = { count: this.#in_constructor_obj.count + 1 }; + this.declared_in_constructor = $state.raw(0, { + onchange(){ + console.log("declared in constructor"); + } + }); + this.declared_in_constructor_obj = $state.raw({ count: 0 }, { + onchange(){ + console.log("declared in constructor object"); + } + }); + this.#assign_in_constructor = $state.raw(0, { + onchange(){ + console.log("assign in constructor"); + } + }); + this.#assign_in_constructor++; + this.#assign_in_constructor_obj = $state.raw({ count: 0 }, { + onchange(){ + console.log("assign in constructor object"); + } + }); + this.#assign_in_constructor_obj = { count: this.#assign_in_constructor_obj.count + 1 }; } } @@ -58,6 +84,9 @@ + + +