fix: put expressions in effects unless known to be static (#15792)

* fix: put expressions in effects unless known to be static

* update test

* handle cycles

* fix
pull/16034/head
Rich Harris 3 months ago committed by GitHub
parent 7a8cb378a1
commit 23d42fb8ac
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194

@ -0,0 +1,5 @@
---
'svelte': patch
---
fix: put expressions in effects unless known to be static

@ -90,7 +90,10 @@ export function Identifier(node, context) {
if (binding) { if (binding) {
if (context.state.expression) { if (context.state.expression) {
context.state.expression.dependencies.add(binding); context.state.expression.dependencies.add(binding);
context.state.expression.has_state ||= binding.kind !== 'normal'; context.state.expression.has_state ||=
binding.kind !== 'static' &&
!binding.is_function() &&
!context.state.scope.evaluate(node).is_known;
} }
if ( if (

@ -221,6 +221,8 @@ class Evaluation {
* @param {Set<any>} values * @param {Set<any>} values
*/ */
constructor(scope, expression, values) { constructor(scope, expression, values) {
current_evaluations.set(expression, this);
this.values = values; this.values = values;
switch (expression.type) { switch (expression.type) {
@ -543,6 +545,8 @@ class Evaluation {
if (this.values.size > 1 || typeof this.value === 'symbol') { if (this.values.size > 1 || typeof this.value === 'symbol') {
this.is_known = false; this.is_known = false;
} }
current_evaluations.delete(expression);
} }
} }
@ -734,10 +738,20 @@ export class Scope {
* @param {Set<any>} [values] * @param {Set<any>} [values]
*/ */
evaluate(expression, values = new Set()) { evaluate(expression, values = new Set()) {
const current = current_evaluations.get(expression);
if (current) return current;
return new Evaluation(this, expression, values); return new Evaluation(this, expression, values);
} }
} }
/**
* Track which expressions are currently being evaluated this allows
* us to prevent cyclical evaluations without passing the map around
* @type {Map<Expression | FunctionDeclaration, Evaluation>}
*/
const current_evaluations = new Map();
/** @type {Record<BinaryOperator, (left: any, right: any) => any>} */ /** @type {Record<BinaryOperator, (left: any, right: any) => any>} */
const binary = { const binary = {
'!=': (left, right) => left != right, '!=': (left, right) => left != right,
@ -1139,7 +1153,7 @@ export function create_scopes(ast, root, allow_reactive_declarations, parent) {
const is_keyed = const is_keyed =
node.key && node.key &&
(node.key.type !== 'Identifier' || !node.index || node.key.name !== node.index); (node.key.type !== 'Identifier' || !node.index || node.key.name !== node.index);
scope.declare(b.id(node.index), is_keyed ? 'template' : 'normal', 'const', node); scope.declare(b.id(node.index), is_keyed ? 'template' : 'static', 'const', node);
} }
if (node.key) visit(node.key, { scope }); if (node.key) visit(node.key, { scope });

@ -265,7 +265,8 @@ export type BindingKind =
| 'snippet' // A snippet parameter | 'snippet' // A snippet parameter
| 'store_sub' // A $store value | 'store_sub' // A $store value
| 'legacy_reactive' // A `$:` declaration | 'legacy_reactive' // A `$:` declaration
| 'template'; // A binding declared in the template, e.g. in an `await` block or `const` tag | 'template' // A binding declared in the template, e.g. in an `await` block or `const` tag
| 'static'; // A binding whose value is known to be static (i.e. each index)
export type DeclarationKind = export type DeclarationKind =
| 'var' | 'var'

@ -0,0 +1,13 @@
import { flushSync } from '../../../../src/index-client';
import { test } from '../../test';
export default test({
html: `<button>0</button>`,
test({ assert, target }) {
const btn = target.querySelector('button');
flushSync(() => btn?.click());
assert.htmlEqual(target.innerHTML, `<button>1</button>`);
}
});

@ -0,0 +1,13 @@
<script>
let count = $state(0);
let object = {
toString() {
return count;
}
};
</script>
<button onclick={() => count += 1}>
{object}
</button>

@ -9,10 +9,16 @@ export default function Main($$anchor) {
let y = () => 'test'; let y = () => 'test';
var fragment = root(); var fragment = root();
var div = $.first_child(fragment); var div = $.first_child(fragment);
$.set_attribute(div, 'foobar', x);
var svg = $.sibling(div, 2); var svg = $.sibling(div, 2);
$.set_attribute(svg, 'viewBox', x);
var custom_element = $.sibling(svg, 2); var custom_element = $.sibling(svg, 2);
$.template_effect(() => $.set_custom_element_data(custom_element, 'fooBar', x)); $.set_custom_element_data(custom_element, 'fooBar', x);
var div_1 = $.sibling(custom_element, 2); var div_1 = $.sibling(custom_element, 2);
var svg_1 = $.sibling(div_1, 2); var svg_1 = $.sibling(div_1, 2);
@ -22,8 +28,6 @@ export default function Main($$anchor) {
$.template_effect( $.template_effect(
($0, $1) => { ($0, $1) => {
$.set_attribute(div, 'foobar', x);
$.set_attribute(svg, 'viewBox', x);
$.set_attribute(div_1, 'foobar', $0); $.set_attribute(div_1, 'foobar', $0);
$.set_attribute(svg_1, 'viewBox', $1); $.set_attribute(svg_1, 'viewBox', $1);
}, },

Loading…
Cancel
Save