out-of-order-rendering
Rich Harris 2 days ago
parent c2e28fb4c8
commit b71126f30b

@ -555,8 +555,7 @@ export function analyze_component(root, source, options) {
snippets: new Set(),
async_deriveds: new Set(),
pickled_awaits: new Set(),
awaited_statements: new Map(),
promise_indexes: new Map()
awaited_statements: new Map()
};
if (!runes) {

@ -26,12 +26,12 @@ export function IfBlock(node, context) {
}
// TODO helperise
const promise_index = Array.from(node.metadata.expression.dependencies).reduce(
(index, binding) => Math.max(index, context.state.analysis.promise_indexes.get(binding) ?? -1),
-1
);
const blockers = new Set();
for (const d of node.metadata.expression.dependencies) {
if (d.blocker) blockers.add(d.blocker);
}
const is_async = promise_index > -1 || node.metadata.expression.has_await;
const is_async = blockers.size > 0 || node.metadata.expression.has_await;
const expression = build_expression(context, node.test, node.metadata.expression);
const test = is_async ? b.call('$.get', b.id('$$condition')) : expression;
@ -84,7 +84,7 @@ export function IfBlock(node, context) {
b.call(
'$.async',
context.state.node,
promise_index === -1 ? undefined : b.id(`$$promises[${promise_index}]`),
b.array([...blockers]),
b.array([b.thunk(expression, node.metadata.expression.has_await)]),
b.arrow([context.state.node, b.id('$$condition')], b.block(statements))
)

@ -166,7 +166,7 @@ function transform_body(program, context) {
/** @type {AwaitedStatement[]} */
const deriveds = [];
const { awaited_statements, promise_indexes } = context.state.analysis;
const { awaited_statements } = context.state.analysis;
let awaited = false;
@ -303,19 +303,27 @@ function transform_body(program, context) {
return b.thunk(b.block([/** @type {Statement} */ (s.node)]), s.has_await);
});
out.push(b.var('$$promises', b.call('$.run', b.array(thunks))));
}
var id = b.id('$$promises'); // TODO if we use this technique for fragments, need to deconflict
// console.log('statements', statements);
// console.log('deriveds', deriveds);
out.push(b.var(id, b.call('$.run', b.array(thunks))));
for (let i = 0; i < statements.length; i += 1) {
const s = statements[i];
for (let i = 0; i < statements.length; i += 1) {
const s = statements[i];
for (const binding of s.declarations) {
promise_indexes.set(binding, i);
var blocker = b.member(id, b.literal(i), true);
for (const binding of s.declarations) {
binding.blocker = blocker;
}
}
// TODO we likely need to account for updates that happen after the declaration,
// e.g. `let obj = $state()` followed by a later `obj = {...}`, otherwise
// a synchronous `{obj.foo}` will fail
}
// console.log('statements', statements);
// console.log('deriveds', deriveds);
return out;
}

@ -271,10 +271,7 @@ export function RegularElement(node, context) {
const { value, has_state } = build_attribute_value(
attribute.value,
context,
(value, metadata) =>
metadata.has_call || metadata.has_await
? context.state.memoizer.add(value, metadata)
: value
(value, metadata) => context.state.memoizer.add(value, metadata)
);
const update = build_element_attribute_update(node, node_id, name, value, attributes);
@ -518,8 +515,6 @@ export function build_class_directives_object(
memoizer = context.state.memoizer
) {
let properties = [];
let has_call_or_state = false;
let has_await = false;
const metadata = create_expression_metadata();
@ -528,13 +523,11 @@ export function build_class_directives_object(
const expression = /** @type Expression */ (context.visit(d.expression));
properties.push(b.init(d.name, expression));
has_call_or_state ||= d.metadata.expression.has_call || d.metadata.expression.has_state;
has_await ||= d.metadata.expression.has_await;
}
const directives = b.object(properties);
return has_call_or_state || has_await ? memoizer.add(directives, metadata) : directives;
return memoizer.add(directives, metadata);
}
/**
@ -551,9 +544,6 @@ export function build_style_directives_object(
const normal = b.object([]);
const important = b.object([]);
let has_call_or_state = false;
let has_await = false;
const metadata = create_expression_metadata();
for (const d of style_directives) {
@ -566,14 +556,11 @@ export function build_style_directives_object(
const object = d.modifiers.includes('important') ? important : normal;
object.properties.push(b.init(d.name, expression));
has_call_or_state ||= d.metadata.expression.has_call || d.metadata.expression.has_state;
has_await ||= d.metadata.expression.has_await;
}
const directives = important.properties.length ? b.array([normal, important]) : normal;
return has_call_or_state || has_await ? memoizer.add(directives, metadata) : directives;
returnmemoizer.add(directives, metadata);
}
/**
@ -702,7 +689,7 @@ function build_element_special_value_attribute(
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 ? state.memoizer.add(value, metadata) : value
state.memoizer.add(value, metadata)
);
const evaluated = context.state.scope.evaluate(value);

@ -35,7 +35,7 @@ export function build_attribute_effect(
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 ? memoizer.add(value, metadata) : value
memoizer.add(value, metadata)
);
if (
@ -52,9 +52,7 @@ export function build_attribute_effect(
} else {
let value = /** @type {Expression} */ (context.visit(attribute));
if (attribute.metadata.expression.has_call || attribute.metadata.expression.has_await) {
value = memoizer.add(value, attribute.metadata.expression);
}
value = memoizer.add(value, attribute.metadata.expression);
values.push(b.spread(value));
}
@ -155,9 +153,7 @@ export function build_set_class(element, node_id, attribute, class_directives, c
value = b.call('$.clsx', value);
}
return metadata.has_call || metadata.has_await
? context.state.memoizer.add(value, metadata)
: value;
return context.state.memoizer.add(value, metadata);
});
/** @type {Identifier | undefined} */
@ -227,7 +223,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 ? context.state.memoizer.add(value, metadata) : value
context.state.memoizer.add(value, metadata)
);
/** @type {Identifier | undefined} */

@ -21,11 +21,25 @@ export class Memoizer {
/** @type {Array<{ id: Identifier, expression: Expression }>} */
#async = [];
/** @type {Set<Expression>} */
#blockers = new Set();
/**
* @param {Expression} expression
* @param {ExpressionMetadata} metadata
*/
add(expression, metadata) {
for (const binding of metadata.dependencies) {
if (binding.blocker) {
this.#blockers.add(binding.blocker);
}
}
if (!metadata.has_call && !metadata.has_await) {
// no memoization required
return expression;
}
const id = b.id('#'); // filled in later
(metadata.has_await ? this.#async : this.#sync).push({ id, expression });
@ -40,6 +54,10 @@ export class Memoizer {
});
}
blockers() {
return b.array([...this.#blockers]);
}
deriveds(runes = true) {
return this.#sync.map((memo) =>
b.let(memo.id, b.call(runes ? '$.derived' : '$.derived_safe_equal', b.thunk(memo.expression)))
@ -72,8 +90,7 @@ export function build_template_chunk(
values,
context,
state = context.state,
memoize = (value, metadata) =>
metadata.has_call || metadata.has_await ? state.memoizer.add(value, metadata) : value
memoize = (value, metadata) => state.memoizer.add(value, metadata)
) {
/** @type {Expression[]} */
const expressions = [];
@ -169,6 +186,7 @@ export function build_render_statement(state) {
return b.stmt(
b.call(
'$.template_effect',
memoizer.blockers(),
b.arrow(
ids,
state.update.length === 1 && state.update[0].type === 'ExpressionStatement'

@ -129,6 +129,15 @@ export class Binding {
mutated = false;
reassigned = false;
/**
* Instance-level declarations may follow (or contain) a top-level `await`. In these cases,
* any reads that occur in the template must wait for the corresponding promise to resolve
* otherwise the initial value will not have been assigned
* TODO the blocker is set during transform which feels a bit grubby
* @type {Expression | null}
*/
blocker = null;
/**
*
* @param {Scope} scope

@ -134,10 +134,4 @@ export interface ComponentAnalysis extends Analysis {
* so that we can run the template synchronously
*/
awaited_statements: Map<Statement | ModuleDeclaration | VariableDeclarator, AwaitedStatement>;
/**
* A map that tells us which of the `$$promises` needs to be awaited
* before a particular binding can be accessed
* TODO this gets populated during transform, which feels wrong
*/
promise_indexes: Map<Binding, number>;
}

@ -14,11 +14,11 @@ import { get_boundary } from './boundary.js';
/**
* @param {TemplateNode} node
* @param {Promise<void> | undefined} blocker
* @param {Array<Promise<void>>} blockers
* @param {Array<() => Promise<any>>} expressions
* @param {(anchor: TemplateNode, ...deriveds: Value[]) => void} fn
*/
export function async(node, blocker, expressions, fn) {
export function async(node, blockers, expressions, fn) {
var boundary = get_boundary();
var batch = /** @type {Batch} */ (current_batch);
var blocking = !boundary.is_pending();
@ -36,7 +36,7 @@ export function async(node, blocker, expressions, fn) {
set_hydrate_node(end);
}
flatten(blocker, [], expressions, (values) => {
flatten(blockers, [], expressions, (values) => {
if (was_hydrating) {
set_hydrating(true);
set_hydrate_node(previous_hydrate_node);

@ -33,19 +33,18 @@ import {
set_hydrating,
skip_nodes
} from '../dom/hydration.js';
import { create_text } from '../dom/operations.js';
import { noop } from '../../shared/utils.js';
/**
* @param {Promise<void> | undefined} blocker
* @param {Array<Promise<void>>} blockers
* @param {Array<() => any>} sync
* @param {Array<() => Promise<any>>} async
* @param {(values: Value[]) => any} fn
*/
export function flatten(blocker, sync, async, fn) {
export function flatten(blockers, sync, async, fn) {
const d = is_runes() ? derived : derived_safe_equal;
if (async.length === 0) {
if (async.length === 0 && blockers.length === 0) {
fn(sync.map(d));
return;
}
@ -57,7 +56,7 @@ export function flatten(blocker, sync, async, fn) {
var was_hydrating = hydrating;
Promise.resolve(blocker).then((values) => {
Promise.all(blockers).then(() => {
restore();
const result = Promise.all(async.map((expression) => async_derived(expression)))

@ -362,13 +362,14 @@ export function render_effect(fn, flags = 0) {
}
/**
* @param {Array<Promise<void>>} blockers
* @param {(...expressions: any) => void | (() => void)} fn
* @param {Array<() => any>} sync
* @param {Array<() => Promise<any>>} async
* @param {Promise<void>} [blocker]
*/
export function template_effect(fn, sync = [], async = [], blocker) {
flatten(blocker, sync, async, (values) => {
export function template_effect(blockers, fn, sync = [], async = [], blocker) {
flatten(blockers, sync, async, (values) => {
create_effect(RENDER_EFFECT, () => fn(...values.map(get)), true);
});
}

Loading…
Cancel
Save