fix: coarse reactivity, alternative approach (#16100)

Make sure we track statically visible dependencies and untrack indirect dependencies

Fixes #14351

---------

Co-authored-by: 7nik <kifiranet@gmail.com>
Co-authored-by: Simon H <5968653+dummdidumm@users.noreply.github.com>
pull/16181/head
Rich Harris 3 months ago committed by GitHub
parent d99d872519
commit b224c3fb4b
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194

@ -0,0 +1,5 @@
---
'svelte': patch
---
fix: use compiler-driven reactivity in legacy mode template expressions

@ -63,7 +63,10 @@ function open(parser) {
end: -1,
test: read_expression(parser),
consequent: create_fragment(),
alternate: null
alternate: null,
metadata: {
expression: create_expression_metadata()
}
});
parser.allow_whitespace();
@ -244,7 +247,10 @@ function open(parser) {
error: null,
pending: null,
then: null,
catch: null
catch: null,
metadata: {
expression: create_expression_metadata()
}
});
if (parser.eat('then')) {
@ -326,7 +332,10 @@ function open(parser) {
start,
end: -1,
expression,
fragment: create_fragment()
fragment: create_fragment(),
metadata: {
expression: create_expression_metadata()
}
});
parser.stack.push(block);
@ -461,7 +470,10 @@ function next(parser) {
elseif: true,
test: expression,
consequent: create_fragment(),
alternate: null
alternate: null,
metadata: {
expression: create_expression_metadata()
}
});
parser.stack.push(child);
@ -624,7 +636,10 @@ function special(parser) {
type: 'HtmlTag',
start,
end: parser.index,
expression
expression,
metadata: {
expression: create_expression_metadata()
}
});
return;
@ -699,6 +714,9 @@ function special(parser) {
declarations: [{ type: 'VariableDeclarator', id, init, start: id.start, end: init.end }],
start: start + 2, // start at const, not at @const
end: parser.index - 1
},
metadata: {
expression: create_expression_metadata()
}
});
}
@ -725,6 +743,7 @@ function special(parser) {
end: parser.index,
expression: /** @type {AST.RenderTag['expression']} */ (expression),
metadata: {
expression: create_expression_metadata(),
dynamic: false,
arguments: [],
path: [],

@ -23,5 +23,9 @@ export function AssignmentExpression(node, context) {
}
}
if (context.state.expression) {
context.state.expression.has_assignment = true;
}
context.next();
}

@ -41,5 +41,8 @@ export function AwaitBlock(node, context) {
mark_subtree_dynamic(context.path);
context.next();
context.visit(node.expression, { ...context.state, expression: node.metadata.expression });
if (node.pending) context.visit(node.pending);
if (node.then) context.visit(node.then);
if (node.catch) context.visit(node.catch);
}

@ -32,5 +32,8 @@ export function ConstTag(node, context) {
e.const_tag_invalid_placement(node);
}
context.next();
const declaration = node.declaration.declarations[0];
context.visit(declaration.id);
context.visit(declaration.init, { ...context.state, expression: node.metadata.expression });
}

@ -15,5 +15,5 @@ export function HtmlTag(node, context) {
// unfortunately this is necessary in order to fix invalid HTML
mark_subtree_dynamic(context.path);
context.next();
context.next({ ...context.state, expression: node.metadata.expression });
}

@ -90,6 +90,7 @@ export function Identifier(node, context) {
if (binding) {
if (context.state.expression) {
context.state.expression.dependencies.add(binding);
context.state.expression.references.add(binding);
context.state.expression.has_state ||=
binding.kind !== 'static' &&
!binding.is_function() &&

@ -17,5 +17,11 @@ export function IfBlock(node, context) {
mark_subtree_dynamic(context.path);
context.next();
context.visit(node.test, {
...context.state,
expression: node.metadata.expression
});
context.visit(node.consequent);
if (node.alternate) context.visit(node.alternate);
}

@ -16,5 +16,6 @@ export function KeyBlock(node, context) {
mark_subtree_dynamic(context.path);
context.next();
context.visit(node.expression, { ...context.state, expression: node.metadata.expression });
context.visit(node.fragment);
}

@ -15,8 +15,9 @@ export function MemberExpression(node, context) {
}
}
if (context.state.expression && !is_pure(node, context)) {
context.state.expression.has_state = true;
if (context.state.expression) {
context.state.expression.has_member_expression = true;
context.state.expression.has_state ||= !is_pure(node, context);
}
if (!is_safe_identifier(node, context.state.scope)) {

@ -54,7 +54,7 @@ export function RenderTag(node, context) {
mark_subtree_dynamic(context.path);
context.visit(callee);
context.visit(callee, { ...context.state, expression: node.metadata.expression });
for (const arg of expression.arguments) {
const metadata = create_expression_metadata();

@ -21,5 +21,9 @@ export function UpdateExpression(node, context) {
}
}
if (context.state.expression) {
context.state.expression.has_assignment = true;
}
context.next();
}

@ -13,6 +13,16 @@ export function visit_function(node, context) {
scope: context.state.scope
};
if (context.state.expression) {
for (const [name] of context.state.scope.references) {
const binding = context.state.scope.get(name);
if (binding && binding.scope.function_depth < context.state.scope.function_depth) {
context.state.expression.references.add(binding);
}
}
}
context.next({
...context.state,
function_depth: context.state.function_depth + 1,

@ -1,21 +1,14 @@
/** @import { Expression } from 'estree' */
/** @import { AST } from '#compiler' */
/** @import { ComponentContext } from '../types' */
import * as b from '../../../../utils/builders.js';
import { build_expression } from './shared/utils.js';
/**
* @param {AST.AttachTag} node
* @param {ComponentContext} context
*/
export function AttachTag(node, context) {
context.state.init.push(
b.stmt(
b.call(
'$.attach',
context.state.node,
b.thunk(/** @type {Expression} */ (context.visit(node.expression)))
)
)
);
const expression = build_expression(context, node.expression, node.metadata.expression);
context.state.init.push(b.stmt(b.call('$.attach', context.state.node, b.thunk(expression))));
context.next();
}

@ -1,10 +1,11 @@
/** @import { BlockStatement, Expression, Pattern, Statement } from 'estree' */
/** @import { BlockStatement, Pattern, Statement } from 'estree' */
/** @import { AST } from '#compiler' */
/** @import { ComponentClientTransformState, ComponentContext } from '../types' */
import { extract_identifiers } from '../../../../utils/ast.js';
import * as b from '#compiler/builders';
import { create_derived } from '../utils.js';
import { get_value } from './shared/declarations.js';
import { build_expression } from './shared/utils.js';
/**
* @param {AST.AwaitBlock} node
@ -14,7 +15,7 @@ export function AwaitBlock(node, context) {
context.state.template.push_comment();
// Visit {#await <expression>} first to ensure that scopes are in the correct order
const expression = b.thunk(/** @type {Expression} */ (context.visit(node.expression)));
const expression = b.thunk(build_expression(context, node.expression, node.metadata.expression));
let then_block;
let catch_block;

@ -1,4 +1,4 @@
/** @import { Expression, Pattern } from 'estree' */
/** @import { Pattern } from 'estree' */
/** @import { AST } from '#compiler' */
/** @import { ComponentContext } from '../types' */
import { dev } from '../../../../state.js';
@ -6,6 +6,7 @@ import { extract_identifiers } from '../../../../utils/ast.js';
import * as b from '#compiler/builders';
import { create_derived } from '../utils.js';
import { get_value } from './shared/declarations.js';
import { build_expression } from './shared/utils.js';
/**
* @param {AST.ConstTag} node
@ -15,15 +16,8 @@ export function ConstTag(node, context) {
const declaration = node.declaration.declarations[0];
// TODO we can almost certainly share some code with $derived(...)
if (declaration.id.type === 'Identifier') {
context.state.init.push(
b.const(
declaration.id,
create_derived(
context.state,
b.thunk(/** @type {Expression} */ (context.visit(declaration.init)))
)
)
);
const init = build_expression(context, declaration.init, node.metadata.expression);
context.state.init.push(b.const(declaration.id, create_derived(context.state, b.thunk(init))));
context.state.transform[declaration.id.name] = { read: get_value };
@ -48,13 +42,15 @@ export function ConstTag(node, context) {
// TODO optimise the simple `{ x } = y` case — we can just return `y`
// instead of destructuring it only to return a new object
const init = build_expression(
{ ...context, state: child_state },
declaration.init,
node.metadata.expression
);
const fn = b.arrow(
[],
b.block([
b.const(
/** @type {Pattern} */ (context.visit(declaration.id, child_state)),
/** @type {Expression} */ (context.visit(declaration.init, child_state))
),
b.const(/** @type {Pattern} */ (context.visit(declaration.id, child_state)), init),
b.return(b.object(identifiers.map((node) => b.prop('init', node, node))))
])
);

@ -1,4 +1,4 @@
/** @import { BlockStatement, Expression, Identifier, Pattern, SequenceExpression, Statement } from 'estree' */
/** @import { BlockStatement, Expression, Identifier, Pattern, Statement } from 'estree' */
/** @import { AST, Binding } from '#compiler' */
/** @import { ComponentContext } from '../types' */
/** @import { Scope } from '../../../scope' */
@ -12,8 +12,8 @@ import {
import { dev } from '../../../../state.js';
import { extract_paths, object } from '../../../../utils/ast.js';
import * as b from '#compiler/builders';
import { build_getter } from '../utils.js';
import { get_value } from './shared/declarations.js';
import { build_expression } from './shared/utils.js';
/**
* @param {AST.EachBlock} node
@ -24,11 +24,18 @@ export function EachBlock(node, context) {
// expression should be evaluated in the parent scope, not the scope
// created by the each block itself
const collection = /** @type {Expression} */ (
context.visit(node.expression, {
...context.state,
scope: /** @type {Scope} */ (context.state.scope.parent)
})
const parent_scope_state = {
...context.state,
scope: /** @type {Scope} */ (context.state.scope.parent)
};
const collection = build_expression(
{
...context,
state: parent_scope_state
},
node.expression,
node.metadata.expression
);
if (!each_node_meta.is_controlled) {

@ -1,8 +1,8 @@
/** @import { Expression } from 'estree' */
/** @import { AST } from '#compiler' */
/** @import { ComponentContext } from '../types' */
import { is_ignored } from '../../../../state.js';
import * as b from '#compiler/builders';
import { build_expression } from './shared/utils.js';
/**
* @param {AST.HtmlTag} node
@ -11,7 +11,7 @@ import * as b from '#compiler/builders';
export function HtmlTag(node, context) {
context.state.template.push_comment();
const expression = /** @type {Expression} */ (context.visit(node.expression));
const expression = build_expression(context, node.expression, node.metadata.expression);
const is_svg = context.state.metadata.namespace === 'svg';
const is_mathml = context.state.metadata.namespace === 'mathml';

@ -2,6 +2,7 @@
/** @import { AST } from '#compiler' */
/** @import { ComponentContext } from '../types' */
import * as b from '#compiler/builders';
import { build_expression } from './shared/utils.js';
/**
* @param {AST.IfBlock} node
@ -31,6 +32,8 @@ export function IfBlock(node, context) {
statements.push(b.var(b.id(alternate_id), b.arrow(alternate_args, alternate)));
}
const test = build_expression(context, node.test, node.metadata.expression);
/** @type {Expression[]} */
const args = [
node.elseif ? b.id('$$anchor') : context.state.node,
@ -38,7 +41,7 @@ export function IfBlock(node, context) {
[b.id('$$render')],
b.block([
b.if(
/** @type {Expression} */ (context.visit(node.test)),
test,
b.stmt(b.call(b.id('$$render'), b.id(consequent_id))),
alternate_id ? b.stmt(b.call(b.id('$$render'), b.id(alternate_id), b.false)) : undefined
)

@ -2,6 +2,7 @@
/** @import { AST } from '#compiler' */
/** @import { ComponentContext } from '../types' */
import * as b from '#compiler/builders';
import { build_expression } from './shared/utils.js';
/**
* @param {AST.KeyBlock} node
@ -10,7 +11,7 @@ import * as b from '#compiler/builders';
export function KeyBlock(node, context) {
context.state.template.push_comment();
const key = /** @type {Expression} */ (context.visit(node.expression));
const key = build_expression(context, node.expression, node.metadata.expression);
const body = /** @type {Expression} */ (context.visit(node.fragment));
context.state.init.push(

@ -331,7 +331,7 @@ export function RegularElement(node, context) {
trimmed.some((node) => node.type === 'ExpressionTag');
if (use_text_content) {
const { value } = build_template_chunk(trimmed, context.visit, child_state);
const { value } = build_template_chunk(trimmed, context, child_state);
const empty_string = value.type === 'Literal' && value.value === '';
if (!empty_string) {

@ -3,6 +3,7 @@
/** @import { ComponentContext } from '../types' */
import { unwrap_optional } from '../../../../utils/ast.js';
import * as b from '#compiler/builders';
import { build_expression } from './shared/utils.js';
/**
* @param {AST.RenderTag} node
@ -19,7 +20,10 @@ export function RenderTag(node, context) {
/** @type {Expression[]} */
let args = [];
for (let i = 0; i < raw_args.length; i++) {
let thunk = b.thunk(/** @type {Expression} */ (context.visit(raw_args[i])));
let thunk = b.thunk(
build_expression(context, /** @type {Expression} */ (raw_args[i]), node.metadata.arguments[i])
);
const { has_call } = node.metadata.arguments[i];
if (has_call) {
@ -31,7 +35,11 @@ export function RenderTag(node, context) {
}
}
let snippet_function = /** @type {Expression} */ (context.visit(callee));
let snippet_function = build_expression(
context,
/** @type {Expression} */ (callee),
node.metadata.expression
);
if (node.metadata.dynamic) {
// If we have a chain expression then ensure a nullish snippet function gets turned into an empty one

@ -10,8 +10,7 @@ import { build_template_chunk } from './shared/utils.js';
export function TitleElement(node, context) {
const { has_state, value } = build_template_chunk(
/** @type {any} */ (node.fragment.nodes),
context.visit,
context.state
context
);
const statement = b.stmt(b.assignment('=', b.id('$.document.title'), value));

@ -7,7 +7,7 @@ import { is_ignored } from '../../../../../state.js';
import { is_event_attribute } from '../../../../../utils/ast.js';
import * as b from '#compiler/builders';
import { build_class_directives_object, build_style_directives_object } from '../RegularElement.js';
import { build_template_chunk, get_expression_id } from './utils.js';
import { build_expression, build_template_chunk, get_expression_id } from './utils.js';
/**
* @param {Array<AST.Attribute | AST.SpreadAttribute>} attributes
@ -121,7 +121,7 @@ export function build_attribute_value(value, context, memoize = (value) => value
return { value: b.literal(chunk.data), has_state: false };
}
let expression = /** @type {Expression} */ (context.visit(chunk.expression));
let expression = build_expression(context, chunk.expression, chunk.metadata.expression);
return {
value: memoize(expression, chunk.metadata.expression),
@ -129,7 +129,7 @@ export function build_attribute_value(value, context, memoize = (value) => value
};
}
return build_template_chunk(value, context.visit, context.state, memoize);
return build_template_chunk(value, context, context.state, memoize);
}
/**

@ -16,8 +16,8 @@ import { build_template_chunk } from './utils.js';
* @param {boolean} is_element
* @param {ComponentContext} context
*/
export function process_children(nodes, initial, is_element, { visit, state }) {
const within_bound_contenteditable = state.metadata.bound_contenteditable;
export function process_children(nodes, initial, is_element, context) {
const within_bound_contenteditable = context.state.metadata.bound_contenteditable;
let prev = initial;
let skipped = 0;
@ -48,8 +48,8 @@ export function process_children(nodes, initial, is_element, { visit, state }) {
let id = expression;
if (id.type !== 'Identifier') {
id = b.id(state.scope.generate(name));
state.init.push(b.var(id, expression));
id = b.id(context.state.scope.generate(name));
context.state.init.push(b.var(id, expression));
}
prev = () => id;
@ -64,13 +64,13 @@ export function process_children(nodes, initial, is_element, { visit, state }) {
function flush_sequence(sequence) {
if (sequence.every((node) => node.type === 'Text')) {
skipped += 1;
state.template.push_text(sequence);
context.state.template.push_text(sequence);
return;
}
state.template.push_text([{ type: 'Text', data: ' ', raw: ' ', start: -1, end: -1 }]);
context.state.template.push_text([{ type: 'Text', data: ' ', raw: ' ', start: -1, end: -1 }]);
const { has_state, value } = build_template_chunk(sequence, visit, state);
const { has_state, value } = build_template_chunk(sequence, context);
// if this is a standalone `{expression}`, make sure we handle the case where
// no text node was created because the expression was empty during SSR
@ -80,9 +80,9 @@ export function process_children(nodes, initial, is_element, { visit, state }) {
const update = b.stmt(b.call('$.set_text', id, value));
if (has_state && !within_bound_contenteditable) {
state.update.push(update);
context.state.update.push(update);
} else {
state.init.push(b.stmt(b.assignment('=', b.member(id, 'nodeValue'), value)));
context.state.init.push(b.stmt(b.assignment('=', b.member(id, 'nodeValue'), value)));
}
}
@ -95,18 +95,18 @@ export function process_children(nodes, initial, is_element, { visit, state }) {
sequence = [];
}
let child_state = state;
let child_state = context.state;
if (is_static_element(node, state)) {
if (is_static_element(node, context.state)) {
skipped += 1;
} else if (node.type === 'EachBlock' && nodes.length === 1 && is_element) {
node.metadata.is_controlled = true;
} else {
const id = flush_node(false, node.type === 'RegularElement' ? node.name : 'node');
child_state = { ...state, node: id };
child_state = { ...context.state, node: id };
}
visit(node, child_state);
context.visit(node, child_state);
}
}
@ -118,7 +118,7 @@ export function process_children(nodes, initial, is_element, { visit, state }) {
// traverse to the last (n - 1) one when hydrating
if (skipped > 1) {
skipped -= 1;
state.init.push(b.stmt(b.call('$.next', skipped !== 1 && b.literal(skipped))));
context.state.init.push(b.stmt(b.call('$.next', skipped !== 1 && b.literal(skipped))));
}
}

@ -1,6 +1,6 @@
/** @import { AssignmentExpression, Expression, ExpressionStatement, Identifier, MemberExpression, SequenceExpression, Literal, Super, UpdateExpression } from 'estree' */
/** @import { AssignmentExpression, Expression, ExpressionStatement, Identifier, MemberExpression, SequenceExpression, Literal, Super, UpdateExpression, Pattern } from 'estree' */
/** @import { AST, ExpressionMetadata } from '#compiler' */
/** @import { ComponentClientTransformState, Context } from '../../types' */
/** @import { ComponentClientTransformState, ComponentContext, Context } from '../../types' */
import { walk } from 'zimmerframe';
import { object } from '../../../../../utils/ast.js';
import * as b from '#compiler/builders';
@ -8,7 +8,7 @@ import { sanitize_template_string } from '../../../../../utils/sanitize_template
import { regex_is_valid_identifier } from '../../../../patterns.js';
import is_reference from 'is-reference';
import { dev, is_ignored, locator } from '../../../../../state.js';
import { create_derived } from '../../utils.js';
import { build_getter, create_derived } from '../../utils.js';
/**
* @param {ComponentClientTransformState} state
@ -31,15 +31,15 @@ export function get_expression_id(expressions, value) {
/**
* @param {Array<AST.Text | AST.ExpressionTag>} values
* @param {(node: AST.SvelteNode, state: any) => any} visit
* @param {ComponentContext} context
* @param {ComponentClientTransformState} state
* @param {(value: Expression, metadata: ExpressionMetadata) => Expression} memoize
* @returns {{ value: Expression, has_state: boolean }}
*/
export function build_template_chunk(
values,
visit,
state,
context,
state = context.state,
memoize = (value, metadata) =>
metadata.has_call ? get_expression_id(state.expressions, value) : value
) {
@ -66,7 +66,7 @@ export function build_template_chunk(
state.scope.get('undefined')
) {
let value = memoize(
/** @type {Expression} */ (visit(node.expression, state)),
build_expression(context, node.expression, node.metadata.expression, state),
node.metadata.expression
);
@ -360,3 +360,48 @@ export function validate_mutation(node, context, expression) {
loc && b.literal(loc.column)
);
}
/**
*
* @param {ComponentContext} context
* @param {Expression} expression
* @param {ExpressionMetadata} metadata
*/
export function build_expression(context, expression, metadata, state = context.state) {
const value = /** @type {Expression} */ (context.visit(expression, state));
if (context.state.analysis.runes) {
return value;
}
if (!metadata.has_call && !metadata.has_member_expression && !metadata.has_assignment) {
return value;
}
// Legacy reactivity is coarse-grained, looking at the statically visible dependencies. Replicate that here
const sequence = b.sequence([]);
for (const binding of metadata.references) {
if (binding.kind === 'normal' && binding.declaration_kind !== 'import') {
continue;
}
var getter = build_getter({ ...binding.node }, state);
if (
binding.kind === 'bindable_prop' ||
binding.kind === 'template' ||
binding.declaration_kind === 'import' ||
binding.node.name === '$$props' ||
binding.node.name === '$$restProps'
) {
getter = b.call('$.deep_read_state', getter);
}
sequence.expressions.push(getter);
}
sequence.expressions.push(b.call('$.untrack', b.thunk(value)));
return sequence;
}

@ -62,8 +62,11 @@ export function create_attribute(name, start, end, value) {
export function create_expression_metadata() {
return {
dependencies: new Set(),
references: new Set(),
has_state: false,
has_call: false
has_call: false,
has_member_expression: false,
has_assignment: false
};
}

@ -279,12 +279,18 @@ export type DeclarationKind =
| 'synthetic';
export interface ExpressionMetadata {
/** All the bindings that are referenced inside this expression */
/** All the bindings that are referenced eagerly (not inside functions) in this expression */
dependencies: Set<Binding>;
/** All the bindings that are referenced inside this expression, including inside functions */
references: Set<Binding>;
/** True if the expression references state directly, or _might_ (via member/call expressions) */
has_state: boolean;
/** True if the expression involves a call expression (often, it will need to be wrapped in a derived) */
has_call: boolean;
/** True if the expression includes a member expression */
has_member_expression: boolean;
/** True if the expression includes an assignment or an update */
has_assignment: boolean;
}
export interface StateField {

@ -135,6 +135,10 @@ export namespace AST {
export interface HtmlTag extends BaseNode {
type: 'HtmlTag';
expression: Expression;
/** @internal */
metadata: {
expression: ExpressionMetadata;
};
}
/** An HTML comment */
@ -151,6 +155,10 @@ export namespace AST {
declaration: VariableDeclaration & {
declarations: [VariableDeclarator & { id: Pattern; init: Expression }];
};
/** @internal */
metadata: {
expression: ExpressionMetadata;
};
}
/** A `{@debug ...}` tag */
@ -165,6 +173,7 @@ export namespace AST {
expression: SimpleCallExpression | (ChainExpression & { expression: SimpleCallExpression });
/** @internal */
metadata: {
expression: ExpressionMetadata;
dynamic: boolean;
arguments: ExpressionMetadata[];
path: SvelteNode[];
@ -447,6 +456,10 @@ export namespace AST {
test: Expression;
consequent: Fragment;
alternate: Fragment | null;
/** @internal */
metadata: {
expression: ExpressionMetadata;
};
}
/** An `{#await ...}` block */
@ -461,12 +474,20 @@ export namespace AST {
pending: Fragment | null;
then: Fragment | null;
catch: Fragment | null;
/** @internal */
metadata: {
expression: ExpressionMetadata;
};
}
export interface KeyBlock extends BaseNode {
type: 'KeyBlock';
expression: Expression;
fragment: Fragment;
/** @internal */
metadata: {
expression: ExpressionMetadata;
};
}
export interface SnippetBlock extends BaseNode {

@ -0,0 +1,12 @@
import { flushSync } from 'svelte';
import { test } from '../../test';
export default test({
test({ assert, target }) {
const button = target.querySelector('button');
assert.htmlEqual(target.innerHTML, `<div></div><button>inc</button> [0,0,0,0,0,0,0,0,0]`);
flushSync(() => button?.click());
assert.htmlEqual(target.innerHTML, `<div></div><button>inc</button> [0,0,0,0,0,0,0,0,0]`);
}
});

@ -0,0 +1,45 @@
<script>
let a = 0, b = 0, c = 0, d = 0, e = 0, f = 0, g = 0, h = 0, i = 0;
function inc() {
a++;
b++;
c++;
d++;
e++;
f++;
g++;
h++;
i++;
}
</script>
{#if a = 0}{/if}
{#each [b = 0] as x}{x,''}{/each}
{#key c = 0}{/key}
{#await d = 0}{/await}
{#snippet snip()}{/snippet}
{@render (e = 0, snip)()}
{@html f = 0, ''}
<div {@attach !!(g = 0)}></div>
{#key 1}
{@const x = (h = 0)}
{x, ''}
{/key}
{#if 1}
{@const x = (i = 0)}
{x, ''}
{/if}
<button on:click={inc}>inc</button>
[{a},{b},{c},{d},{e},{f},{g},{h},{i}]

@ -0,0 +1,12 @@
import { flushSync } from 'svelte';
import { test } from '../../test';
export default test({
test({ assert, target }) {
const button = target.querySelector('button');
assert.htmlEqual(target.innerHTML, `<div></div><button data-foo="true">inc</button> 12 - 12`);
flushSync(() => button?.click());
assert.htmlEqual(target.innerHTML, `<div></div><button data-foo="true">inc</button> 13 - 12`);
}
});

@ -0,0 +1,36 @@
<script>
let count1 = 1;
let count2 = 1;
function fn(ret) {
if (count1 > 100) return ret;
count1++;
count2++;
return ret;
}
</script>
{#if fn(false)}{:else if fn(true)}{/if}
{#each fn([]) as x}{x, ''}{/each}
{#key fn(1)}{/key}
{#await fn(Promise.resolve())}{/await}
{#snippet snip()}{/snippet}
{@render fn(snip)()}
{@html fn('')}
<div {@attach fn(() => {})}></div>
{#key 1}
{@const x = fn('')}
{x}
{/key}
<button data-foo={fn(true)} on:click={() => count1++}>{fn('inc')}</button>
{count1} - {count2}

@ -0,0 +1,12 @@
import { flushSync } from 'svelte';
import { test } from '../../test';
export default test({
test({ assert, target }) {
const button = target.querySelector('button');
assert.htmlEqual(target.innerHTML, `<div></div><button>inc</button> 10 - 10`);
flushSync(() => button?.click());
assert.htmlEqual(target.innerHTML, `<div></div><button>inc</button> 11 - 10`);
}
});

@ -0,0 +1,46 @@
<script>
let count1 = 1;
let count2 = 1;
function fn(ret) {
if (count1 > 100) return ret;
count1++;
count2++;
return ret;
}
const obj = {
get true() { return fn(true) },
get false() { return fn(false) },
get array() { return fn([]) },
get string() { return fn('') },
get promise() { return fn(Promise.resolve()) },
get snippet() { return fn(snip) },
get attachment() { return fn(() => {}) },
}
</script>
{#if obj.false}{:else if obj.true}{/if}
{#each obj.array as x}{x, ''}{/each}
{#key obj.string}{/key}
{#await obj.promise}{/await}
{#snippet snip()}{/snippet}
{@render obj.snippet()}
{@html obj.string}
<div {@attach obj.attachment}></div>
{#key 1}
{@const x = obj.string}
{x}
{/key}
<button on:click={() => count1++}>inc</button>
{count1} - {count2}

@ -5,7 +5,7 @@
export let index;
export let n;
function logRender () {
function logRender (n) {
order.push(`${index}: render ${n}`);
return index;
}
@ -24,5 +24,5 @@
</script>
<li>
{logRender()}
{logRender(n)}
</li>

@ -5,7 +5,7 @@
export let n = 0;
function logRender () {
function logRender (n) {
order.push(`parent: render ${n}`);
return 'parent';
}
@ -23,7 +23,7 @@
})
</script>
{logRender()}
{logRender(n)}
<ul>
{#each [1,2,3] as index}
<Item {index} {n} />

@ -8,11 +8,13 @@ export default function Purity($$anchor) {
var fragment = root();
var p = $.first_child(fragment);
p.textContent = '0';
p.textContent = (
$.untrack(() => Math.max(0, Math.min(0, 100)))
);
var p_1 = $.sibling(p, 2);
p_1.textContent = location.href;
p_1.textContent = ($.untrack(() => location.href));
var node = $.sibling(p_1, 2);

Loading…
Cancel
Save