pull/15844/head
Rich Harris 2 months ago
parent 12fffb9c1c
commit 2b4410007d

@ -59,6 +59,7 @@ import { UpdateExpression } from './visitors/UpdateExpression.js';
import { UseDirective } from './visitors/UseDirective.js';
import { AttachTag } from './visitors/AttachTag.js';
import { VariableDeclaration } from './visitors/VariableDeclaration.js';
import { Memoizer } from './visitors/shared/utils.js';
/** @type {Visitors} */
const visitors = {
@ -170,10 +171,9 @@ export function client_component(analysis, options) {
// these are set inside the `Fragment` visitor, and cannot be used until then
init: /** @type {any} */ (null),
update: /** @type {any} */ (null),
expressions: /** @type {any} */ (null),
async_expressions: /** @type {any} */ (null),
after_update: /** @type {any} */ (null),
template: /** @type {any} */ (null)
template: /** @type {any} */ (null),
memoizer: /** @type {any} */ (null)
};
const module = /** @type {ESTree.Program} */ (

@ -12,6 +12,7 @@ import type { AST, Namespace, ValidatedCompileOptions } from '#compiler';
import type { TransformState } from '../types.js';
import type { ComponentAnalysis } from '../../types.js';
import type { Template } from './transform-template/template.js';
import type { Memoizer } from './visitors/shared/utils.js';
export interface ClientTransformState extends TransformState {
/**
@ -49,10 +50,8 @@ export interface ComponentClientTransformState extends ClientTransformState {
readonly update: Statement[];
/** Stuff that happens after the render effect (control blocks, dynamic elements, bindings, actions, etc) */
readonly after_update: Statement[];
/** Expressions used inside the render effect */
readonly expressions: Array<{ id: Identifier; expression: Expression }>;
/** Expressions used inside the render effect */
readonly async_expressions: Array<{ id: Identifier; expression: Expression }>;
/** Memoized expressions */
readonly memoizer: Memoizer;
/** The HTML template string */
readonly template: Template;
readonly metadata: {
@ -87,8 +86,3 @@ export type ComponentVisitors = import('zimmerframe').Visitors<
AST.SvelteNode,
ComponentClientTransformState
>;
export interface MemoizedExpression {
id: Identifier;
expression: Expression;
}

@ -6,7 +6,7 @@ import * as b from '#compiler/builders';
import { clean_nodes, infer_namespace } from '../../utils.js';
import { transform_template } from '../transform-template/index.js';
import { process_children } from './shared/fragment.js';
import { build_render_statement } from './shared/utils.js';
import { build_render_statement, Memoizer } from './shared/utils.js';
import { Template } from '../transform-template/template.js';
/**
@ -62,9 +62,8 @@ export function Fragment(node, context) {
...context.state,
init: [],
update: [],
expressions: [],
async_expressions: [],
after_update: [],
memoizer: new Memoizer(),
template: new Template(),
transform: { ...context.state.transform },
metadata: {

@ -22,7 +22,7 @@ import {
build_set_style
} from './shared/element.js';
import { process_children } from './shared/fragment.js';
import { build_render_statement, build_template_chunk, get_expression_id } from './shared/utils.js';
import { build_render_statement, build_template_chunk, Memoizer } from './shared/utils.js';
import { visit_event_attribute } from './shared/events.js';
/**
@ -255,10 +255,7 @@ export function RegularElement(node, context) {
context,
(value, metadata) =>
metadata.has_call || metadata.has_await
? get_expression_id(
metadata.has_await ? context.state.async_expressions : context.state.expressions,
value
)
? context.state.memoizer.add(value, metadata.has_await)
: value
);
@ -465,15 +462,13 @@ function setup_select_synchronization(value_binding, context) {
/**
* @param {AST.ClassDirective[]} class_directives
* @param {ComponentContext} context
* @param {MemoizedExpression[]} async_expressions
* @param {MemoizedExpression[]} expressions
* @param {Memoizer} memoizer
* @return {ObjectExpression | Identifier}
*/
export function build_class_directives_object(
class_directives,
context,
async_expressions = context.state.async_expressions,
expressions = context.state.expressions
memoizer = context.state.memoizer
) {
let properties = [];
let has_call_or_state = false;
@ -488,23 +483,19 @@ export function build_class_directives_object(
const directives = b.object(properties);
return has_call_or_state || has_await
? get_expression_id(has_await ? async_expressions : expressions, directives)
: directives;
return has_call_or_state || has_await ? memoizer.add(directives, has_await) : directives;
}
/**
* @param {AST.StyleDirective[]} style_directives
* @param {ComponentContext} context
* @param {MemoizedExpression[]} async_expressions
* @param {MemoizedExpression[]} expressions
* @param {Memoizer} memoizer
* @return {ObjectExpression | ArrayExpression | Identifier}}
*/
export function build_style_directives_object(
style_directives,
context,
async_expressions = context.state.async_expressions,
expressions = context.state.expressions
memoizer = context.state.memoizer
) {
const normal = b.object([]);
const important = b.object([]);
@ -527,9 +518,7 @@ export function build_style_directives_object(
const directives = important.properties.length ? b.array([normal, important]) : normal;
return has_call_or_state || has_await
? get_expression_id(has_await ? async_expressions : expressions, directives)
: directives;
return has_call_or_state || has_await ? memoizer.add(directives, has_await) : directives;
}
/**
@ -651,9 +640,7 @@ function build_element_special_value_attribute(element, node_id, attribute, cont
element === 'select' && attribute.value !== true && !is_text_attribute(attribute);
const { value, has_state } = build_attribute_value(attribute.value, context, (value, metadata) =>
metadata.has_call || metadata.has_await
? get_expression_id(metadata.has_await ? state.async_expressions : state.expressions, value)
: value
metadata.has_call || metadata.has_await ? state.memoizer.add(value, metadata.has_await) : value
);
const evaluated = context.state.scope.evaluate(value);

@ -1,10 +1,10 @@
/** @import { Expression, Statement } from 'estree' */
/** @import { AST } from '#compiler' */
/** @import { ComponentContext, MemoizedExpression } from '../types' */
/** @import { ComponentContext } from '../types' */
import { unwrap_optional } from '../../../../utils/ast.js';
import * as b from '#compiler/builders';
import { create_derived } from '../utils.js';
import { get_expression_id, build_expression } from './shared/utils.js';
import { build_expression, Memoizer } from './shared/utils.js';
/**
* @param {AST.RenderTag} node
@ -18,11 +18,7 @@ export function RenderTag(node, context) {
/** @type {Expression[]} */
let args = [];
/** @type {MemoizedExpression[]} */
const expressions = [];
/** @type {MemoizedExpression[]} */
const async_expressions = [];
const memoizer = new Memoizer();
for (let i = 0; i < call.arguments.length; i++) {
const arg = /** @type {Expression} */ (call.arguments[i]);
@ -31,21 +27,16 @@ export function RenderTag(node, context) {
let expression = build_expression(context, arg, metadata);
if (metadata.has_await || metadata.has_call) {
expression = b.call(
'$.get',
get_expression_id(metadata.has_await ? async_expressions : expressions, expression)
);
expression = b.call('$.get', memoizer.add(expression, metadata.has_await));
}
args.push(b.thunk(expression));
}
[...async_expressions, ...expressions].forEach((memo, i) => {
memo.id.name = `$${i}`;
});
memoizer.apply();
/** @type {Statement[]} */
const statements = expressions.map((memo) =>
const statements = memoizer.sync.map((memo) =>
b.var(memo.id, create_derived(context.state, b.thunk(memo.expression)))
);
@ -76,15 +67,15 @@ export function RenderTag(node, context) {
);
}
if (async_expressions.length > 0) {
if (memoizer.async.length > 0) {
context.state.init.push(
b.stmt(
b.call(
'$.async',
context.state.node,
b.array(async_expressions.map((memo) => b.thunk(memo.expression, true))),
b.array(memoizer.async.map((memo) => b.thunk(memo.expression, true))),
b.arrow(
[context.state.node, ...async_expressions.map((memo) => memo.id)],
[context.state.node, ...memoizer.async.map((memo) => memo.id)],
b.block(statements)
)
)

@ -1,10 +1,10 @@
/** @import { BlockStatement, Expression, ExpressionStatement, Literal, Property, Statement } from 'estree' */
/** @import { AST } from '#compiler' */
/** @import { ComponentContext, MemoizedExpression } from '../types' */
/** @import { ComponentContext } from '../types' */
import * as b from '#compiler/builders';
import { create_derived } from '../utils.js';
import { build_attribute_value } from './shared/element.js';
import { get_expression_id } from './shared/utils.js';
import { Memoizer } from './shared/utils.js';
/**
* @param {AST.SlotElement} node
@ -23,11 +23,7 @@ export function SlotElement(node, context) {
/** @type {ExpressionStatement[]} */
const lets = [];
/** @type {MemoizedExpression[]} */
const expressions = [];
/** @type {MemoizedExpression[]} */
const async_expressions = [];
const memoizer = new Memoizer();
let name = b.literal('default');
@ -40,10 +36,7 @@ export function SlotElement(node, context) {
context,
(value, metadata) =>
metadata.has_call || metadata.has_await
? b.call(
'$.get',
get_expression_id(metadata.has_await ? async_expressions : expressions, value)
)
? b.call('$.get', memoizer.add(value, metadata.has_await))
: value
);
@ -61,15 +54,13 @@ export function SlotElement(node, context) {
}
}
[...async_expressions, ...expressions].forEach((memo, i) => {
memo.id.name = `$${i}`;
});
memoizer.apply();
// Let bindings first, they can be used on attributes
context.state.init.push(...lets);
/** @type {Statement[]} */
const statements = expressions.map((memo) =>
const statements = memoizer.sync.map((memo) =>
b.var(memo.id, create_derived(context.state, b.thunk(memo.expression)))
);
@ -85,15 +76,15 @@ export function SlotElement(node, context) {
b.stmt(b.call('$.slot', context.state.node, b.id('$$props'), name, props_expression, fallback))
);
if (async_expressions.length > 0) {
if (memoizer.async.length > 0) {
context.state.init.push(
b.stmt(
b.call(
'$.async',
context.state.node,
b.array(async_expressions.map((memo) => b.thunk(memo.expression, true))),
b.array(memoizer.async.map((memo) => b.thunk(memo.expression, true))),
b.arrow(
[context.state.node, ...async_expressions.map((memo) => memo.id)],
[context.state.node, ...memoizer.async.map((memo) => memo.id)],
b.block(statements)
)
)

@ -10,7 +10,7 @@ import {
build_attribute_effect,
build_set_class
} from './shared/element.js';
import { build_render_statement } from './shared/utils.js';
import { build_render_statement, Memoizer } from './shared/utils.js';
/**
* @param {AST.SvelteElement} node
@ -46,9 +46,8 @@ export function SvelteElement(node, context) {
node: element_id,
init: [],
update: [],
expressions: [],
async_expressions: [],
after_update: []
after_update: [],
memoizer: new Memoizer()
}
};

@ -1,10 +1,10 @@
/** @import { BlockStatement, Expression, ExpressionStatement, Identifier, MemberExpression, Pattern, Property, SequenceExpression, Statement } from 'estree' */
/** @import { AST } from '#compiler' */
/** @import { ComponentContext, MemoizedExpression } from '../../types.js' */
/** @import { ComponentContext } from '../../types.js' */
import { dev, is_ignored } from '../../../../../state.js';
import { get_attribute_chunks, object } from '../../../../../utils/ast.js';
import * as b from '#compiler/builders';
import { build_bind_this, get_expression_id, validate_binding } from '../shared/utils.js';
import { build_bind_this, Memoizer, validate_binding } from '../shared/utils.js';
import { build_attribute_value } from '../shared/element.js';
import { build_event_handler } from './events.js';
import { determine_slot } from '../../../../../utils/slot.js';
@ -44,11 +44,7 @@ export function build_component(node, component_name, context) {
/** @type {Record<string, Expression[]>} */
const events = {};
/** @type {MemoizedExpression[]} */
const expressions = [];
/** @type {MemoizedExpression[]} */
const async_expressions = [];
const memoizer = new Memoizer();
/** @type {Property[]} */
const custom_css_props = [];
@ -139,13 +135,7 @@ export function build_component(node, component_name, context) {
props_and_spreads.push(
b.thunk(
attribute.metadata.expression.has_await || attribute.metadata.expression.has_call
? b.call(
'$.get',
get_expression_id(
attribute.metadata.expression.has_await ? async_expressions : expressions,
expression
)
)
? b.call('$.get', memoizer.add(expression, attribute.metadata.expression.has_await))
: expression
)
);
@ -160,10 +150,7 @@ export function build_component(node, component_name, context) {
build_attribute_value(attribute.value, context, (value, metadata) => {
// TODO put the derived in the local block
return metadata.has_call || metadata.has_await
? b.call(
'$.get',
get_expression_id(metadata.has_await ? async_expressions : expressions, value)
)
? b.call('$.get', memoizer.add(value, metadata.has_await))
: value;
}).value
)
@ -199,10 +186,7 @@ export function build_component(node, component_name, context) {
});
return should_wrap_in_derived
? b.call(
'$.get',
get_expression_id(metadata.has_await ? async_expressions : expressions, value)
)
? b.call('$.get', memoizer.add(value, metadata.has_await))
: value;
}
);
@ -465,7 +449,7 @@ export function build_component(node, component_name, context) {
const statements = [
...snippet_declarations,
...expressions.map((memo) =>
...memoizer.sync.map((memo) =>
b.let(memo.id, create_derived(context.state, b.thunk(memo.expression)))
)
];
@ -515,17 +499,15 @@ export function build_component(node, component_name, context) {
statements.push(b.stmt(fn(anchor)));
}
[...async_expressions, ...expressions].forEach((memo, i) => {
memo.id.name = `$${i}`;
});
memoizer.apply();
if (async_expressions.length > 0) {
if (memoizer.async.length > 0) {
return b.stmt(
b.call(
'$.async',
anchor,
b.array(async_expressions.map(({ expression }) => b.thunk(expression, true))),
b.arrow([b.id('$$anchor'), ...async_expressions.map(({ id }) => id)], b.block(statements))
b.array(memoizer.async.map(({ expression }) => b.thunk(expression, true))),
b.arrow([b.id('$$anchor'), ...memoizer.async.map(({ id }) => id)], b.block(statements))
)
);
}

@ -1,13 +1,13 @@
/** @import { ArrayExpression, Expression, Identifier, ObjectExpression } from 'estree' */
/** @import { Expression, Identifier, ObjectExpression } from 'estree' */
/** @import { AST, ExpressionMetadata } from '#compiler' */
/** @import { ComponentContext, MemoizedExpression } from '../../types' */
/** @import { ComponentContext } from '../../types' */
import { escape_html } from '../../../../../../escaping.js';
import { normalize_attribute } from '../../../../../../utils.js';
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_expression, build_template_chunk, get_expression_id } from './utils.js';
import { build_expression, build_template_chunk, Memoizer } from './utils.js';
/**
* @param {Array<AST.Attribute | AST.SpreadAttribute>} attributes
@ -28,18 +28,12 @@ export function build_attribute_effect(
/** @type {ObjectExpression['properties']} */
const values = [];
/** @type {MemoizedExpression[]} */
const async_expressions = [];
/** @type {MemoizedExpression[]} */
const expressions = [];
const memoizer = new Memoizer();
for (const attribute of attributes) {
if (attribute.type === 'Attribute') {
const { value } = build_attribute_value(attribute.value, context, (value, metadata) =>
metadata.has_call || metadata.has_await
? get_expression_id(metadata.has_await ? async_expressions : expressions, value)
: value
metadata.has_call || metadata.has_await ? memoizer.add(value, metadata.has_await) : value
);
if (
@ -57,10 +51,7 @@ export function build_attribute_effect(
let value = /** @type {Expression} */ (context.visit(attribute));
if (attribute.metadata.expression.has_call || attribute.metadata.expression.has_await) {
value = get_expression_id(
attribute.metadata.expression.has_await ? async_expressions : expressions,
value
);
value = memoizer.add(value, attribute.metadata.expression.has_await);
}
values.push(b.spread(value));
@ -72,7 +63,7 @@ export function build_attribute_effect(
b.prop(
'init',
b.array([b.id('$.CLASS')]),
build_class_directives_object(class_directives, context, async_expressions, expressions)
build_class_directives_object(class_directives, context, memoizer)
)
);
}
@ -82,16 +73,12 @@ export function build_attribute_effect(
b.prop(
'init',
b.array([b.id('$.STYLE')]),
build_style_directives_object(style_directives, context, async_expressions, expressions)
build_style_directives_object(style_directives, context, memoizer)
)
);
}
const all = [...expressions, ...async_expressions];
for (let i = 0; i < all.length; i += 1) {
all[i].id.name = `$${i}`;
}
const all = memoizer.apply();
context.state.init.push(
b.stmt(
@ -102,9 +89,10 @@ export function build_attribute_effect(
all.map(({ id }) => id),
b.object(values)
),
expressions.length > 0 && b.array(expressions.map(({ expression }) => b.thunk(expression))),
async_expressions.length > 0 &&
b.array(async_expressions.map(({ expression }) => b.thunk(expression, true))),
memoizer.sync.length > 0 &&
b.array(memoizer.sync.map(({ expression }) => b.thunk(expression))),
memoizer.async.length > 0 &&
b.array(memoizer.async.map(({ expression }) => b.thunk(expression, true))),
element.metadata.scoped &&
context.state.analysis.css.hash !== '' &&
b.literal(context.state.analysis.css.hash),
@ -170,10 +158,7 @@ export function build_set_class(element, node_id, attribute, class_directives, c
}
return metadata.has_call || metadata.has_await
? get_expression_id(
metadata.has_await ? context.state.async_expressions : context.state.expressions,
value
)
? context.state.memoizer.add(value, metadata.has_await)
: value;
});
@ -244,12 +229,7 @@ export function build_set_class(element, node_id, attribute, class_directives, c
*/
export function build_set_style(node_id, attribute, style_directives, context) {
let { value, has_state } = build_attribute_value(attribute.value, context, (value, metadata) =>
metadata.has_call
? get_expression_id(
metadata.has_await ? context.state.async_expressions : context.state.expressions,
value
)
: value
metadata.has_call ? context.state.memoizer.add(value, metadata.has_await) : value
);
/** @type {Identifier | undefined} */
@ -258,7 +238,7 @@ export function build_set_style(node_id, attribute, style_directives, context) {
/** @type {ObjectExpression | Identifier | undefined} */
let prev;
/** @type {ArrayExpression | ObjectExpression | undefined} */
/** @type {Expression | undefined} */
let next;
if (style_directives.length) {

@ -1,6 +1,6 @@
/** @import { AssignmentExpression, Expression, Identifier, MemberExpression, SequenceExpression, Literal, Super, UpdateExpression } from 'estree' */
/** @import { AST, ExpressionMetadata } from '#compiler' */
/** @import { ComponentClientTransformState, ComponentContext, Context, MemoizedExpression } from '../../types' */
/** @import { ComponentClientTransformState, ComponentContext, Context } from '../../types' */
import { walk } from 'zimmerframe';
import { object } from '../../../../../utils/ast.js';
import * as b from '#compiler/builders';
@ -10,16 +10,34 @@ import is_reference from 'is-reference';
import { dev, is_ignored, locator } from '../../../../../state.js';
import { build_getter } from '../../utils.js';
/**
*
* @param {MemoizedExpression[]} expressions
* @param {Expression} expression
*/
export function get_expression_id(expressions, expression) {
const id = b.id(`#`); // filled in later
expressions.push({ id, expression });
export class Memoizer {
/** @type {Array<{ id: Identifier, expression: Expression }>} */
sync = [];
/** @type {Array<{ id: Identifier, expression: Expression }>} */
async = [];
/**
* @param {Expression} expression
* @param {boolean} has_await
*/
add(expression, has_await) {
const id = b.id(`#`); // filled in later
(has_await ? this.async : this.sync).push({ id, expression });
return id;
return id;
}
apply() {
const all = [...this.async, ...this.sync];
all.forEach((memo, i) => {
memo.id.name = `$${i}`;
});
return all;
}
}
/**
@ -34,9 +52,7 @@ export function build_template_chunk(
context,
state = context.state,
memoize = (value, metadata) =>
metadata.has_call || metadata.has_await
? get_expression_id(metadata.has_await ? state.async_expressions : state.expressions, value)
: value
metadata.has_call || metadata.has_await ? state.memoizer.add(value, metadata.has_await) : value
) {
/** @type {Expression[]} */
const expressions = [];
@ -125,14 +141,9 @@ export function build_template_chunk(
* @param {ComponentClientTransformState} state
*/
export function build_render_statement(state) {
const sync = state.expressions;
const async = state.async_expressions;
const all = [...sync, ...async];
const { memoizer } = state;
for (let i = 0; i < all.length; i += 1) {
all[i].id.name = `$${i}`;
}
const all = state.memoizer.apply();
return b.stmt(
b.call(
@ -143,8 +154,9 @@ export function build_render_statement(state) {
? state.update[0].expression
: b.block(state.update)
),
all.length > 0 && b.array(sync.map(({ expression }) => b.thunk(expression))),
async.length > 0 && b.array(async.map(({ expression }) => b.thunk(expression, true)))
all.length > 0 && b.array(memoizer.sync.map(({ expression }) => b.thunk(expression))),
memoizer.async.length > 0 &&
b.array(memoizer.async.map(({ expression }) => b.thunk(expression, true)))
)
);
}

Loading…
Cancel
Save