Skip to content

Commit a5d349e

Browse files
authored
feat: compiler-driven each block reactivity (#12744)
* WIP * note to self * WIP * fix * fix * delete unwrap and is_signal * simplify * remove some junk * regenerate * reinstate key-is-item optimisation
1 parent cbcd761 commit a5d349e

File tree

12 files changed

+39
-83
lines changed

12 files changed

+39
-83
lines changed

packages/svelte/src/compiler/phases/2-analyze/visitors/EachBlock.js

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -29,6 +29,7 @@ export function EachBlock(node, context) {
2929
// evaluate expression in parent scope
3030
context.visit(node.expression, {
3131
...context.state,
32+
expression: node.metadata.expression,
3233
scope: /** @type {Scope} */ (context.state.scope.parent)
3334
});
3435

packages/svelte/src/compiler/phases/2-analyze/visitors/Identifier.js

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -115,10 +115,10 @@ export function Identifier(node, context) {
115115
}
116116
}
117117

118-
if (binding && binding.kind !== 'normal') {
118+
if (binding) {
119119
if (context.state.expression) {
120120
context.state.expression.dependencies.add(binding);
121-
context.state.expression.has_state = true;
121+
context.state.expression.has_state ||= binding.kind !== 'normal';
122122
}
123123

124124
if (

packages/svelte/src/compiler/phases/3-transform/client/utils.js

Lines changed: 0 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -269,20 +269,6 @@ export function should_proxy_or_freeze(node, scope) {
269269
return true;
270270
}
271271

272-
/**
273-
* Port over the location information from the source to the target identifier.
274-
* but keep the target as-is (i.e. a new id is created).
275-
* This ensures esrap can generate accurate source maps.
276-
* @param {Identifier} target
277-
* @param {Identifier} source
278-
*/
279-
export function with_loc(target, source) {
280-
if (source.loc) {
281-
return { ...target, loc: source.loc };
282-
}
283-
return target;
284-
}
285-
286272
/**
287273
* @param {Pattern} node
288274
* @param {import('zimmerframe').Context<SvelteNode, ComponentClientTransformState>} context

packages/svelte/src/compiler/phases/3-transform/client/visitors/EachBlock.js

Lines changed: 28 additions & 28 deletions
Original file line numberDiff line numberDiff line change
@@ -13,7 +13,7 @@ import {
1313
import { dev } from '../../../../state.js';
1414
import { extract_paths, object } from '../../../../utils/ast.js';
1515
import * as b from '../../../../utils/builders.js';
16-
import { build_getter, with_loc } from '../utils.js';
16+
import { build_getter } from '../utils.js';
1717
import { get_value } from './shared/declarations.js';
1818

1919
/**
@@ -48,18 +48,26 @@ export function EachBlock(node, context) {
4848
if (node.index) {
4949
flags |= EACH_INDEX_REACTIVE;
5050
}
51+
}
5152

52-
// In runes mode, if key === item, we don't need to wrap the item in a source
53-
const key_is_item =
54-
/** @type {Expression} */ (node.key).type === 'Identifier' &&
55-
node.context.type === 'Identifier' &&
56-
node.context.name === node.key.name;
53+
const key_is_item =
54+
node.key?.type === 'Identifier' &&
55+
node.context.type === 'Identifier' &&
56+
node.context.name === node.key.name;
57+
58+
for (const binding of node.metadata.expression.dependencies) {
59+
// if the expression doesn't reference any external state, we don't need to
60+
// create a source for the item. TODO cover more cases (e.g. `x.filter(y)`
61+
// should also qualify if `y` doesn't reference state, and non-state
62+
// bindings should also be fine
63+
if (binding.scope.function_depth >= context.state.scope.function_depth) {
64+
continue;
65+
}
5766

58-
if (!context.state.analysis.runes || !key_is_item) {
67+
if (!context.state.analysis.runes || !key_is_item || binding.kind === 'store_sub') {
5968
flags |= EACH_ITEM_REACTIVE;
69+
break;
6070
}
61-
} else {
62-
flags |= EACH_ITEM_REACTIVE;
6371
}
6472

6573
// Since `animate:` can only appear on elements that are the sole child of a keyed each block,
@@ -134,21 +142,13 @@ export function EachBlock(node, context) {
134142
const index =
135143
each_node_meta.contains_group_binding || !node.index ? each_node_meta.index : b.id(node.index);
136144
const item = each_node_meta.item;
137-
const binding = /** @type {Binding} */ (context.state.scope.get(item.name));
138-
const getter = (/** @type {Identifier} */ id) => {
139-
const item_with_loc = with_loc(item, id);
140-
return b.call('$.unwrap', item_with_loc);
141-
};
142145

143146
if (node.index) {
144-
child_state.transform[node.index] = {
145-
read: (id) => {
146-
const index_with_loc = with_loc(index, id);
147-
return (flags & EACH_INDEX_REACTIVE) === 0
148-
? index_with_loc
149-
: b.call('$.get', index_with_loc);
150-
}
151-
};
147+
if ((flags & EACH_INDEX_REACTIVE) !== 0) {
148+
child_state.transform[node.index] = { read: get_value };
149+
} else {
150+
delete child_state.transform[node.index];
151+
}
152152

153153
delete key_state.transform[node.index];
154154
}
@@ -172,7 +172,7 @@ export function EachBlock(node, context) {
172172

173173
if (node.context.type === 'Identifier') {
174174
child_state.transform[node.context.name] = {
175-
read: getter,
175+
read: (flags & EACH_ITEM_REACTIVE) !== 0 ? get_value : (node) => node,
176176
assign: (_, value) => {
177177
const left = b.member(
178178
each_node_meta.array_name ? b.call(each_node_meta.array_name) : collection,
@@ -187,12 +187,12 @@ export function EachBlock(node, context) {
187187

188188
delete key_state.transform[node.context.name];
189189
} else {
190-
const unwrapped = getter(binding.node);
191-
const paths = extract_paths(node.context);
190+
const unwrapped = (flags & EACH_ITEM_REACTIVE) !== 0 ? b.call('$.get', item) : item;
192191

193-
for (const path of paths) {
192+
for (const path of extract_paths(node.context)) {
194193
const name = /** @type {Identifier} */ (path.node).name;
195194
const needs_derived = path.has_default_value; // to ensure that default value is only called once
195+
196196
const fn = b.thunk(
197197
/** @type {Expression} */ (context.visit(path.expression?.(unwrapped), child_state))
198198
);
@@ -203,11 +203,11 @@ export function EachBlock(node, context) {
203203

204204
child_state.transform[name] = {
205205
read,
206-
assign: (node, value) => {
206+
assign: (_, value) => {
207207
const left = /** @type {Pattern} */ (path.update_expression(unwrapped));
208208
return b.sequence([b.assignment('=', left, value), ...sequence]);
209209
},
210-
mutate: (node, mutation) => {
210+
mutate: (_, mutation) => {
211211
return b.sequence([mutation, ...sequence]);
212212
}
213213
};

packages/svelte/src/compiler/phases/3-transform/client/visitors/shared/function.js

Lines changed: 2 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -9,15 +9,11 @@ import { build_hoisted_params } from '../../utils.js';
99
export const visit_function = (node, context) => {
1010
const metadata = node.metadata;
1111

12-
let state = context.state;
12+
let state = { ...context.state, in_constructor: false };
1313

1414
if (node.type === 'FunctionExpression') {
1515
const parent = /** @type {Node} */ (context.path.at(-1));
16-
const in_constructor = parent.type === 'MethodDefinition' && parent.kind === 'constructor';
17-
18-
state = { ...context.state, in_constructor };
19-
} else {
20-
state = { ...context.state, in_constructor: false };
16+
state.in_constructor = parent.type === 'MethodDefinition' && parent.kind === 'constructor';
2117
}
2218

2319
if (metadata?.hoisted === true) {

packages/svelte/src/compiler/phases/scope.js

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@
33
/** @import { AnimateDirective, Binding, Component, DeclarationKind, EachBlock, ElementLike, LetDirective, SvelteComponent, SvelteNode, SvelteSelf, TransitionDirective, UseDirective } from '#compiler' */
44
import is_reference from 'is-reference';
55
import { walk } from 'zimmerframe';
6+
import { create_expression_metadata } from './nodes.js';
67
import * as b from '../utils/builders.js';
78
import * as e from '../errors.js';
89
import {
@@ -574,6 +575,7 @@ export function create_scopes(ast, root, allow_reactive_declarations, parent) {
574575
}
575576

576577
node.metadata = {
578+
expression: create_expression_metadata(),
577579
keyed: false,
578580
contains_group_binding: false,
579581
array_name: needs_array_deduplication ? state.scope.root.unique('$$array') : null,

packages/svelte/src/compiler/types/template.d.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -396,6 +396,7 @@ export interface EachBlock extends BaseNode {
396396
index?: string;
397397
key?: Expression;
398398
metadata: {
399+
expression: ExpressionMetadata;
399400
keyed: boolean;
400401
contains_group_binding: boolean;
401402
/** Set if something in the array expression is shadowed within the each block */

packages/svelte/src/internal/client/dom/blocks/each.js

Lines changed: 0 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -149,11 +149,6 @@ export function each(node, flags, get_collection, get_key, render_fn, fallback_f
149149
!(STATE_SYMBOL in array)
150150
) {
151151
flags ^= EACH_IS_STRICT_EQUALS;
152-
153-
// Additionally if we're in an keyed each block, we'll need ensure the items are all wrapped in signals.
154-
if ((flags & EACH_KEYED) !== 0 && (flags & EACH_ITEM_REACTIVE) === 0) {
155-
flags ^= EACH_ITEM_REACTIVE;
156-
}
157152
}
158153

159154
/** `true` if there was a hydration mismatch. Needs to be a `let` or else it isn't treeshaken out */

packages/svelte/src/internal/client/index.js

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -137,7 +137,6 @@ export {
137137
exclude_from_object,
138138
pop,
139139
push,
140-
unwrap,
141140
deep_read,
142141
deep_read_state,
143142
getAllContexts,

packages/svelte/src/internal/client/runtime.js

Lines changed: 0 additions & 25 deletions
Original file line numberDiff line numberDiff line change
@@ -845,17 +845,6 @@ export function set_signal_status(signal, status) {
845845
signal.f = (signal.f & STATUS_MASK) | status;
846846
}
847847

848-
/**
849-
* @template V
850-
* @param {V | Value<V>} val
851-
* @returns {val is Value<V>}
852-
*/
853-
export function is_signal(val) {
854-
return (
855-
typeof val === 'object' && val !== null && typeof (/** @type {Value<V>} */ (val).f) === 'number'
856-
);
857-
}
858-
859848
/**
860849
* Retrieves the context that belongs to the closest parent component with the specified `key`.
861850
* Must be called during component initialisation.
@@ -1139,20 +1128,6 @@ export function deep_read(value, visited = new Set()) {
11391128
}
11401129
}
11411130

1142-
/**
1143-
* @template V
1144-
* @param {V | Value<V>} value
1145-
* @returns {V}
1146-
*/
1147-
export function unwrap(value) {
1148-
if (is_signal(value)) {
1149-
// @ts-ignore
1150-
return get(value);
1151-
}
1152-
// @ts-ignore
1153-
return value;
1154-
}
1155-
11561131
if (DEV) {
11571132
/**
11581133
* @param {string} rune

0 commit comments

Comments
 (0)