fix: compute teardown values more correctly

teardown-value-fix
Simon Holthausen 1 week ago
parent 4bf15ae6f3
commit 9ca41a3add
No known key found for this signature in database

@ -26,6 +26,8 @@ export interface ClientTransformState extends TransformState {
{ {
/** turn `foo` into e.g. `$.get(foo)` */ /** turn `foo` into e.g. `$.get(foo)` */
read: (id: Identifier) => Expression; read: (id: Identifier) => Expression;
/** turn `foo` into e.g. `$$props.foo` inside the template */
read_template?: (id: Identifier) => Expression;
/** turn `foo = bar` into e.g. `$.set(foo, bar)` */ /** turn `foo = bar` into e.g. `$.set(foo, bar)` */
assign?: (node: Identifier, value: Expression, proxy?: boolean) => Expression; assign?: (node: Identifier, value: Expression, proxy?: boolean) => Expression;
/** turn `foo.bar = baz` into e.g. `$.mutate(foo, $.get(foo).bar = baz);` */ /** turn `foo.bar = baz` into e.g. `$.mutate(foo, $.get(foo).bar = baz);` */

@ -37,7 +37,10 @@ export function build_getter(node, state) {
// don't transform the declaration itself // don't transform the declaration itself
if (node !== binding?.node) { if (node !== binding?.node) {
return state.transform[node.name].read(node); var transform = state.transform[node.name];
return state.is_instance || transform.read_template === undefined
? transform.read(node)
: transform.read_template(node);
} }
} }

@ -16,30 +16,6 @@ export function Identifier(node, context) {
return b.id('$$sanitized_props'); return b.id('$$sanitized_props');
} }
// Optimize prop access: If it's a member read access, we can use the $$props object directly
const binding = context.state.scope.get(node.name);
if (
context.state.analysis.runes && // can't do this in legacy mode because the proxy does more than just read/write
binding !== null &&
node !== binding.node &&
binding.kind === 'rest_prop'
) {
const grand_parent = context.path.at(-2);
if (
parent?.type === 'MemberExpression' &&
!parent.computed &&
grand_parent?.type !== 'AssignmentExpression' &&
grand_parent?.type !== 'UpdateExpression'
) {
const key = /** @type {Identifier} */ (parent.property);
if (!binding.metadata?.exclude_props?.includes(key.name)) {
return b.id('$$props');
}
}
}
return build_getter(node, context.state); return build_getter(node, context.state);
} }
} }

@ -7,6 +7,28 @@ import * as b from '#compiler/builders';
* @param {Context} context * @param {Context} context
*/ */
export function MemberExpression(node, context) { export function MemberExpression(node, context) {
if (
context.state.analysis.runes &&
node.object.type === 'Identifier' &&
node.property.type === 'Identifier' &&
!node.computed
) {
const binding = context.state.scope.get(node.object.name);
const parent = context.path.at(-1);
if (
binding?.kind === 'rest_prop' &&
node.object !== binding.node &&
parent?.type !== 'AssignmentExpression' &&
parent?.type !== 'UpdateExpression' &&
!binding.metadata?.exclude_props?.includes(node.property.name)
) {
return context.state.is_instance
? b.call('$.get_prop_value', b.id('$$props'), b.literal(node.property.name))
: b.member(b.id('$$props'), node.property);
}
}
// rewrite `this.#foo` as `this.#foo.v` inside a constructor // rewrite `this.#foo` as `this.#foo.v` inside a constructor
if (node.property.type === 'PrivateIdentifier') { if (node.property.type === 'PrivateIdentifier') {
const field = context.state.state_fields.get('#' + node.property.name); const field = context.state.state_fields.get('#' + node.property.name);

@ -126,11 +126,13 @@ export function Program(node, context) {
const key = b.key(binding.prop_alias); const key = b.key(binding.prop_alias);
context.state.transform[name] = { context.state.transform[name] = {
read: (_) => b.member(b.id('$$props'), key, key.type === 'Literal') read: (_) => b.call('$.get_prop_value', b.id('$$props'), b.literal(binding.prop_alias)),
read_template: (_) => b.member(b.id('$$props'), key, key.type === 'Literal')
}; };
} else { } else {
context.state.transform[name] = { context.state.transform[name] = {
read: (node) => b.member(b.id('$$props'), node) read: (_) => b.call('$.get_prop_value', b.id('$$props'), b.literal(name)),
read_template: (node) => b.member(b.id('$$props'), node)
}; };
} }
} }

@ -63,7 +63,11 @@ export function SnippetBlock(node, context) {
// we need to eagerly evaluate the expression in order to hit any // we need to eagerly evaluate the expression in order to hit any
// 'Cannot access x before initialization' errors // 'Cannot access x before initialization' errors
if (dev) { if (dev) {
declarations.push(b.stmt(transform[name].read(b.id(name)))); var read =
context.state.is_instance || transform[name].read_template === undefined
? transform[name].read
: transform[name].read_template;
declarations.push(b.stmt(read(b.id(name))));
} }
} }
} }

@ -416,7 +416,11 @@ export function validate_mutation(node, context, expression) {
? context.state.transform[left.property.name] ? context.state.transform[left.property.name]
: null; : null;
if (left.computed) { if (left.computed) {
path.unshift(transform?.read ? transform.read(left.property) : left.property); var read =
state.is_instance || transform?.read_template === undefined
? transform?.read
: transform.read_template;
path.unshift(read ? read(left.property) : left.property);
} else { } else {
path.unshift(b.literal(left.property.name)); path.unshift(b.literal(left.property.name));
} }

@ -2,7 +2,7 @@
import { DESTROYING, STATE_SYMBOL } from '#client/constants'; import { DESTROYING, STATE_SYMBOL } from '#client/constants';
import { component_context, mark_as_component } from '../../../context.js'; import { component_context, mark_as_component } from '../../../context.js';
import { effect, render_effect } from '../../../reactivity/effects.js'; import { effect, render_effect } from '../../../reactivity/effects.js';
import { active_effect, untrack } from '../../../runtime.js'; import { active_effect, untrack, with_old_values } from '../../../runtime.js';
/** /**
* @param {any} bound_value * @param {any} bound_value
@ -67,9 +67,11 @@ export function bind_this(
p = p.parent; p = p.parent;
} }
const teardown = () => { const teardown = () => {
with_old_values(() => {
if (parts && is_bound_this(get_value(...parts), element_or_component)) { if (parts && is_bound_this(get_value(...parts), element_or_component)) {
update(null, ...parts); update(null, ...parts);
} }
});
}; };
const original_teardown = p.teardown; const original_teardown = p.teardown;
p.teardown = () => { p.teardown = () => {

@ -156,6 +156,7 @@ export { invalidate_inner_signals } from './legacy.js';
export { set_text } from './render.js'; export { set_text } from './render.js';
export { export {
get, get,
get_prop_value,
safe_get, safe_get,
tick, tick,
untrack, untrack,

@ -3,12 +3,13 @@ import {
is_dirty, is_dirty,
active_effect, active_effect,
active_reaction, active_reaction,
destroying_effect,
update_effect, update_effect,
get, get,
is_destroying_effect, is_destroying_effect,
remove_reactions, remove_reactions,
set_active_reaction, set_active_reaction,
set_is_destroying_effect, set_destroying_effect,
untrack, untrack,
untracking, untracking,
set_active_effect set_active_effect
@ -445,9 +446,9 @@ export function branch(fn) {
export function execute_effect_teardown(effect) { export function execute_effect_teardown(effect) {
var teardown = effect.teardown; var teardown = effect.teardown;
if (teardown !== null) { if (teardown !== null) {
const previously_destroying_effect = is_destroying_effect; const previous_destroying_effect = destroying_effect;
const previous_reaction = active_reaction; const previous_reaction = active_reaction;
set_is_destroying_effect(true); set_destroying_effect(effect);
set_active_reaction(null); set_active_reaction(null);
try { try {
teardown.call(null); teardown.call(null);
@ -457,7 +458,7 @@ export function execute_effect_teardown(effect) {
// themselves mid-teardown are skipped by invoke_error_boundary. // themselves mid-teardown are skipped by invoke_error_boundary.
invoke_error_boundary(error, effect.parent); invoke_error_boundary(error, effect.parent);
} finally { } finally {
set_is_destroying_effect(previously_destroying_effect); set_destroying_effect(previous_destroying_effect);
set_active_reaction(previous_reaction); set_active_reaction(previous_reaction);
} }
} }

@ -15,7 +15,8 @@ import {
get, get,
is_destroying_effect, is_destroying_effect,
set_active_effect, set_active_effect,
untrack untrack,
with_old_values
} from '../runtime.js'; } from '../runtime.js';
import * as e from '../errors.js'; import * as e from '../errors.js';
import { DESTROYED, LEGACY_PROPS, STATE_SYMBOL } from '#client/constants'; import { DESTROYED, LEGACY_PROPS, STATE_SYMBOL } from '#client/constants';
@ -54,7 +55,7 @@ export function update_pre_prop(fn, d = 1) {
const rest_props_handler = { const rest_props_handler = {
get(target, key) { get(target, key) {
if (target.exclude.has(key)) return; if (target.exclude.has(key)) return;
return target.props[key]; return with_old_values(() => target.props[key]);
}, },
set(target, key) { set(target, key) {
if (DEV) { if (DEV) {
@ -70,7 +71,7 @@ const rest_props_handler = {
return { return {
enumerable: true, enumerable: true,
configurable: true, configurable: true,
value: target.props[key] value: with_old_values(() => target.props[key])
}; };
} }
}, },
@ -359,7 +360,7 @@ export function prop(props, key, flags, fallback) {
// prop is never written to — we only need a getter // prop is never written to — we only need a getter
if (runes && (flags & PROPS_IS_UPDATED) === 0) { if (runes && (flags & PROPS_IS_UPDATED) === 0) {
return getter; return () => with_old_values(getter);
} }
// prop is written to, but the parent component had `bind:foo` which // prop is written to, but the parent component had `bind:foo` which
@ -380,7 +381,7 @@ export function prop(props, key, flags, fallback) {
return value; return value;
} }
return getter(); return with_old_values(getter);
} }
); );
} }
@ -404,6 +405,7 @@ export function prop(props, key, flags, fallback) {
if (bindable) get(d); if (bindable) get(d);
var parent_effect = /** @type {Effect} */ (active_effect); var parent_effect = /** @type {Effect} */ (active_effect);
var get_derived = () => get(d);
return /** @type {() => V} */ ( return /** @type {() => V} */ (
function (/** @type {any} */ value, /** @type {boolean} */ mutation) { function (/** @type {any} */ value, /** @type {boolean} */ mutation) {
@ -427,7 +429,7 @@ export function prop(props, key, flags, fallback) {
return d.v; return d.v;
} }
return get(d); return with_old_values(get_derived);
} }
); );
} }

@ -68,9 +68,51 @@ let is_updating_effect = false;
export let is_destroying_effect = false; export let is_destroying_effect = false;
/** @param {boolean} value */ /** @type {Effect | null} */
export function set_is_destroying_effect(value) { export let destroying_effect = null;
is_destroying_effect = value;
let is_reading_old_value = false;
let old_value_read_version = 0;
/** @param {Effect | null} effect */
export function set_destroying_effect(effect) {
destroying_effect = effect;
is_destroying_effect = effect !== null;
}
/**
* @template V
* @param {() => V} fn
* @returns {V}
*/
export function with_old_values(fn) {
if (!is_destroying_effect) return fn();
var previous_is_reading_old_value = is_reading_old_value;
is_reading_old_value = true;
try {
return fn();
} finally {
is_reading_old_value = previous_is_reading_old_value;
}
}
/**
* @param {Record<string, any>} props
* @param {string} key
*/
export function get_prop_value(props, key) {
if (!is_destroying_effect) return props[key];
var previous_is_reading_old_value = is_reading_old_value;
is_reading_old_value = true;
try {
return props[key];
} finally {
is_reading_old_value = previous_is_reading_old_value;
}
} }
/** @type {null | Reaction} */ /** @type {null | Reaction} */
@ -660,7 +702,12 @@ export function get(signal) {
} }
} }
if (is_destroying_effect && old_values.has(signal)) { if (
is_destroying_effect &&
old_values.has(signal) &&
(is_reading_old_value || reaction_depends_on(/** @type {Effect} */ (destroying_effect), signal))
) {
old_value_read_version += 1;
return old_values.get(signal); return old_values.get(signal);
} }
@ -669,6 +716,7 @@ export function get(signal) {
if (is_destroying_effect) { if (is_destroying_effect) {
var value = derived.v; var value = derived.v;
var previous_old_value_read_version = old_value_read_version;
// if the derived is dirty and has reactions, or depends on the values that just changed, re-execute // if the derived is dirty and has reactions, or depends on the values that just changed, re-execute
// (a derived can be maybe_dirty due to the effect destroy removing its last reaction) // (a derived can be maybe_dirty due to the effect destroy removing its last reaction)
@ -679,7 +727,11 @@ export function get(signal) {
value = execute_derived(derived); value = execute_derived(derived);
} }
// Don't let a current value calculated for one teardown become the old value
// observed by a later teardown in the same flush.
if (old_value_read_version !== previous_old_value_read_version) {
old_values.set(derived, value); old_values.set(derived, value);
}
return value; return value;
} }
@ -759,6 +811,30 @@ function depends_on_old_values(derived) {
return false; return false;
} }
/**
* @param {Reaction} reaction
* @param {Value} signal
* @param {Set<Reaction>} checked
*/
function reaction_depends_on(reaction, signal, checked = new Set()) {
if (reaction.deps === null || checked.has(reaction)) return false;
checked.add(reaction);
for (const dep of reaction.deps) {
if (dep === signal) return true;
if (
(dep.f & DERIVED) !== 0 &&
reaction_depends_on(/** @type {Derived} */ (dep), signal, checked)
) {
return true;
}
}
return false;
}
/** /**
* Like `get`, but checks for `undefined`. Used for `var` declarations because they can be accessed before being declared * Like `get`, but checks for `undefined`. Used for `var` declarations because they can be accessed before being declared
* @template V * @template V

@ -0,0 +1,9 @@
<script>
let { track, value, ...rest } = $props();
$effect(() => {
track;
return () => console.log(`value = ${value}, other = ${rest.other}`);
});
</script>

@ -0,0 +1,19 @@
import { flushSync } from 'svelte';
import { test } from '../../test';
export default test({
compileOptions: {
accessors: false
},
test({ assert, target, logs }) {
const button = target.querySelector('button');
flushSync(() => button?.click());
assert.deepEqual(logs, [
'value = true, other = true',
'track = 0, thing1 = false, thing2 = false, derived = false',
'tracked derived = true',
'tracked derived = false'
]);
}
});

@ -0,0 +1,43 @@
<script>
import Child from './Child.svelte';
let track = $state(0);
let value = $state(true);
let other = $state(true);
let thing1 = $state(true);
let thing2 = true;
let derived = $derived(thing1);
$effect(() => {
track;
return () =>
console.log(
`track = ${track}, thing1 = ${thing1}, thing2 = ${thing2}, derived = ${derived}`
);
});
$effect(() => {
derived;
return () => console.log(`tracked derived = ${derived}`);
});
$effect(() => {
track;
return () => console.log(`tracked derived = ${derived}`);
});
</script>
<button onclick={() => {
value = !value;
other = !other;
thing1 = !thing1;
thing2 = !thing2;
track++;
}}>Re-run effect</button>
{thing1} {thing2}
<Child {track} {value} {other} />

@ -10,6 +10,6 @@ export default test({
}); });
await Promise.resolve(); await Promise.resolve();
assert.deepEqual(logs, ['top level', 'inner', 0, 'destroy inner', 0, 'destroy outer', 0]); assert.deepEqual(logs, ['top level', 'inner', 0, 'destroy inner', 0, 'destroy outer', 1]);
} }
}); });

@ -8,10 +8,10 @@ export default function Props_identifier($$anchor, $$props) {
let props = $.rest_props($$props, rest_excludes); let props = $.rest_props($$props, rest_excludes);
$$props.a; $.get_prop_value($$props, 'a');
props[a]; props[a];
$$props.a.b; $.get_prop_value($$props, 'a').b;
$$props.a.b = true; $.get_prop_value($$props, 'a').b = true;
props.a = true; props.a = true;
props[a] = true; props[a] = true;
props; props;

Loading…
Cancel
Save