chore: refactor task microtask dispatching + boundary scheduling

pull/15046/head
Dominic Gannaway 8 months ago
parent a9d1f46dbb
commit 0dcc250a00

@ -0,0 +1,5 @@
---
'svelte': patch
---
chore: refactor task microtask dispatching + boundary scheduling

@ -2,7 +2,7 @@
/** @import { Context } from '../types' */
import * as e from '../../../errors.js';
const valid = ['onerror', 'failed'];
const valid = ['onerror', 'failed', 'pending'];
/**
* @param {AST.SvelteBoundary} node

@ -39,7 +39,10 @@ export function SvelteBoundary(node, context) {
// Capture the `failed` implicit snippet prop
for (const child of node.fragment.nodes) {
if (child.type === 'SnippetBlock' && child.expression.name === 'failed') {
if (
child.type === 'SnippetBlock' &&
(child.expression.name === 'failed' || child.expression.name === 'pending')
) {
// we need to delay the visit of the snippets in case they access a ConstTag that is declared
// after the snippets so that the visitor for the const tag can be updated
snippets_visits.push(() => {

@ -13,7 +13,7 @@ import {
set_dev_current_component_function
} from '../../runtime.js';
import { hydrate_next, hydrate_node, hydrating } from '../hydration.js';
import { queue_micro_task } from '../task.js';
import { queue_post_micro_task } from '../task.js';
import { UNINITIALIZED } from '../../../../constants.js';
const PENDING = 0;
@ -148,7 +148,7 @@ export function await_block(node, get_input, pending_fn, then_fn, catch_fn) {
} else {
// Wait a microtask before checking if we should show the pending state as
// the promise might have resolved by the next microtask.
queue_micro_task(() => {
queue_post_micro_task(() => {
if (!resolved) update(PENDING, true);
});
}

@ -1,7 +1,13 @@
/** @import { Effect, TemplateNode, } from '#client' */
import { BOUNDARY_EFFECT, EFFECT_TRANSPARENT } from '../../constants.js';
import { block, branch, destroy_effect, pause_effect } from '../../reactivity/effects.js';
import {
block,
branch,
destroy_effect,
pause_effect,
resume_effect
} from '../../reactivity/effects.js';
import {
active_effect,
active_reaction,
@ -20,7 +26,11 @@ import {
remove_nodes,
set_hydrate_node
} from '../hydration.js';
import { queue_micro_task } from '../task.js';
import { get_next_sibling } from '../operations.js';
import { queue_boundary_micro_task } from '../task.js';
const ASYNC_INCREMENT = Symbol();
const ASYNC_DECREMENT = Symbol();
/**
* @param {Effect} boundary
@ -49,6 +59,7 @@ function with_boundary(boundary, fn) {
* @param {{
* onerror?: (error: unknown, reset: () => void) => void,
* failed?: (anchor: Node, error: () => unknown, reset: () => () => void) => void
* pending?: (anchor: Node) => void
* }} props
* @param {((anchor: Node) => void)} boundary_fn
* @returns {void}
@ -58,14 +69,106 @@ export function boundary(node, props, boundary_fn) {
/** @type {Effect} */
var boundary_effect;
/** @type {Effect | null} */
var async_effect = null;
/** @type {DocumentFragment | null} */
var async_fragment = null;
var async_count = 0;
block(() => {
var boundary = /** @type {Effect} */ (active_effect);
var hydrate_open = hydrate_node;
var is_creating_fallback = false;
// We re-use the effect's fn property to avoid allocation of an additional field
boundary.fn = (/** @type {unknown}} */ error) => {
const render_snippet = (/** @type { () => void } */ snippet_fn) => {
with_boundary(boundary, () => {
is_creating_fallback = true;
try {
boundary_effect = branch(() => {
snippet_fn();
});
} catch (error) {
handle_error(error, boundary, null, boundary.ctx);
}
reset_is_throwing_error();
is_creating_fallback = false;
});
};
// @ts-ignore We re-use the effect's fn property to avoid allocation of an additional field
boundary.fn = (/** @type {unknown} */ input) => {
let pending = props.pending;
if (input === ASYNC_INCREMENT) {
if (!pending) {
return false;
}
if (async_count++ === 0) {
queue_boundary_micro_task(() => {
if (async_effect || !boundary_effect) {
return;
}
var effect = boundary_effect;
async_effect = boundary_effect;
pause_effect(
async_effect,
() => {
/** @type {TemplateNode | null} */
var node = effect.nodes_start;
var end = effect.nodes_end;
async_fragment = document.createDocumentFragment();
while (node !== null) {
/** @type {TemplateNode | null} */
var sibling =
node === end ? null : /** @type {TemplateNode} */ (get_next_sibling(node));
node.remove();
async_fragment.append(node);
node = sibling;
}
},
false
);
render_snippet(() => {
pending(anchor);
});
});
}
return true;
}
if (input === ASYNC_DECREMENT) {
if (!pending) {
return false;
}
if (--async_count === 0) {
queue_boundary_micro_task(() => {
if (!async_effect) {
return;
}
if (boundary_effect) {
destroy_effect(boundary_effect);
}
boundary_effect = async_effect;
async_effect = null;
anchor.before(/** @type {DocumentFragment} */ (async_fragment));
resume_effect(boundary_effect);
});
}
return true;
}
var error = input;
var onerror = props.onerror;
let failed = props.failed;
@ -96,25 +199,13 @@ export function boundary(node, props, boundary_fn) {
}
if (failed) {
// Render the `failed` snippet in a microtask
queue_micro_task(() => {
with_boundary(boundary, () => {
is_creating_fallback = true;
try {
boundary_effect = branch(() => {
failed(
anchor,
() => error,
() => reset
);
});
} catch (error) {
handle_error(error, boundary, null, boundary.ctx);
}
reset_is_throwing_error();
is_creating_fallback = false;
queue_boundary_micro_task(() => {
render_snippet(() => {
failed(
anchor,
() => error,
() => reset
);
});
});
}
@ -132,3 +223,21 @@ export function boundary(node, props, boundary_fn) {
anchor = hydrate_node;
}
}
/**
* @param {Effect | null} effect
* @param {typeof ASYNC_INCREMENT | typeof ASYNC_DECREMENT} trigger
*/
export function trigger_async_boundary(effect, trigger) {
var current = effect;
while (current !== null) {
if ((current.f & BOUNDARY_EFFECT) !== 0) {
// @ts-ignore
if (current.fn(trigger)) {
return;
}
}
current = current.parent;
}
}

@ -34,7 +34,7 @@ import {
import { source, mutable_source, internal_set } from '../../reactivity/sources.js';
import { array_from, is_array } from '../../../shared/utils.js';
import { INERT } from '../../constants.js';
import { queue_micro_task } from '../task.js';
import { queue_post_micro_task } from '../task.js';
import { active_effect, active_reaction, get } from '../../runtime.js';
import { DEV } from 'esm-env';
import { derived_safe_equal } from '../../reactivity/deriveds.js';
@ -470,7 +470,7 @@ function reconcile(array, state, anchor, render_fn, flags, is_inert, get_key, ge
}
if (is_animated) {
queue_micro_task(() => {
queue_post_micro_task(() => {
if (to_animate === undefined) return;
for (item of to_animate) {
item.a?.apply();

@ -1,5 +1,5 @@
import { DEV } from 'esm-env';
import { queue_micro_task } from './task.js';
import { queue_post_micro_task } from './task.js';
import { register_style } from '../dev/css.js';
/**
@ -7,8 +7,8 @@ import { register_style } from '../dev/css.js';
* @param {{ hash: string, code: string }} css
*/
export function append_styles(anchor, css) {
// Use `queue_micro_task` to ensure `anchor` is in the DOM, otherwise getRootNode() will yield wrong results
queue_micro_task(() => {
// Use `queue_post_micro_task` to ensure `anchor` is in the DOM, otherwise getRootNode() will yield wrong results
queue_post_micro_task(() => {
var root = anchor.getRootNode();
var target = /** @type {ShadowRoot} */ (root).host

@ -3,7 +3,7 @@ import { render_effect, teardown } from '../../../reactivity/effects.js';
import { listen_to_event_and_reset_event } from './shared.js';
import * as e from '../../../errors.js';
import { is } from '../../../proxy.js';
import { queue_micro_task } from '../../task.js';
import { queue_post_micro_task } from '../../task.js';
import { hydrating } from '../../hydration.js';
import { is_runes, untrack } from '../../../runtime.js';
@ -158,14 +158,14 @@ export function bind_group(inputs, group_index, input, get, set = get) {
if (!pending.has(binding_group)) {
pending.add(binding_group);
queue_micro_task(() => {
queue_post_micro_task(() => {
// necessary to maintain binding group order in all insertion scenarios
binding_group.sort((a, b) => (a.compareDocumentPosition(b) === 4 ? -1 : 1));
pending.delete(binding_group);
});
}
queue_micro_task(() => {
queue_post_micro_task(() => {
if (hydration_mismatch) {
var value;

@ -1,7 +1,7 @@
import { STATE_SYMBOL } from '../../../constants.js';
import { effect, render_effect } from '../../../reactivity/effects.js';
import { untrack } from '../../../runtime.js';
import { queue_micro_task } from '../../task.js';
import { queue_post_micro_task } from '../../task.js';
/**
* @param {any} bound_value
@ -49,7 +49,7 @@ export function bind_this(element_or_component = {}, update, get_value, get_part
return () => {
// We cannot use effects in the teardown phase, we we use a microtask instead.
queue_micro_task(() => {
queue_post_micro_task(() => {
if (parts && is_bound_this(get_value(...parts), element_or_component)) {
update(null, ...parts);
}

@ -2,7 +2,7 @@
import { teardown } from '../../reactivity/effects.js';
import { define_property, is_array } from '../../../shared/utils.js';
import { hydrating } from '../hydration.js';
import { queue_micro_task } from '../task.js';
import { queue_post_micro_task } from '../task.js';
import { FILENAME } from '../../../../constants.js';
import * as w from '../../warnings.js';
import {
@ -77,7 +77,7 @@ export function create_event(event_name, dom, handler, options) {
event_name.startsWith('touch') ||
event_name === 'wheel'
) {
queue_micro_task(() => {
queue_post_micro_task(() => {
dom.addEventListener(event_name, target_handler, options);
});
} else {

@ -1,6 +1,6 @@
import { hydrating } from '../hydration.js';
import { clear_text_content, get_first_child } from '../operations.js';
import { queue_micro_task } from '../task.js';
import { queue_post_micro_task } from '../task.js';
/**
* @param {HTMLElement} dom
@ -12,7 +12,7 @@ export function autofocus(dom, value) {
const body = document.body;
dom.autofocus = true;
queue_micro_task(() => {
queue_post_micro_task(() => {
if (document.activeElement === body) {
dom.focus();
}

@ -13,7 +13,7 @@ import { should_intro } from '../../render.js';
import { current_each_item } from '../blocks/each.js';
import { TRANSITION_GLOBAL, TRANSITION_IN, TRANSITION_OUT } from '../../../../constants.js';
import { BLOCK_EFFECT, EFFECT_RAN, EFFECT_TRANSPARENT } from '../../constants.js';
import { queue_micro_task } from '../task.js';
import { queue_post_micro_task } from '../task.js';
/**
* @param {Element} element
@ -326,7 +326,7 @@ function animate(element, options, counterpart, t2, on_finish) {
var a;
var aborted = false;
queue_micro_task(() => {
queue_post_micro_task(() => {
if (aborted) return;
var o = options({ direction: is_intro ? 'in' : 'out' });
a = animate(element, o, counterpart, t2, on_finish);

@ -10,54 +10,70 @@ let is_micro_task_queued = false;
let is_idle_task_queued = false;
/** @type {Array<() => void>} */
let current_queued_micro_tasks = [];
let queued_boundary_microtasks = [];
/** @type {Array<() => void>} */
let current_queued_idle_tasks = [];
let queued_post_microtasks = [];
/** @type {Array<() => void>} */
let queued_idle_tasks = [];
function process_micro_tasks() {
is_micro_task_queued = false;
const tasks = current_queued_micro_tasks.slice();
current_queued_micro_tasks = [];
export function flush_boundary_micro_tasks() {
const tasks = queued_boundary_microtasks.slice();
queued_boundary_microtasks = [];
run_all(tasks);
}
function process_idle_tasks() {
is_idle_task_queued = false;
const tasks = current_queued_idle_tasks.slice();
current_queued_idle_tasks = [];
export function flush_post_micro_tasks() {
const tasks = queued_post_microtasks.slice();
queued_post_microtasks = [];
run_all(tasks);
}
export function flush_idle_tasks() {
if (is_idle_task_queued) {
is_idle_task_queued = false;
const tasks = queued_idle_tasks.slice();
queued_idle_tasks = [];
run_all(tasks);
}
}
function flush_all_micro_tasks() {
if (is_micro_task_queued) {
is_micro_task_queued = false;
flush_boundary_micro_tasks();
flush_post_micro_tasks();
}
}
/**
* @param {() => void} fn
*/
export function queue_micro_task(fn) {
export function queue_boundary_micro_task(fn) {
if (!is_micro_task_queued) {
is_micro_task_queued = true;
queueMicrotask(process_micro_tasks);
queueMicrotask(flush_all_micro_tasks);
}
current_queued_micro_tasks.push(fn);
queued_boundary_microtasks.push(fn);
}
/**
* @param {() => void} fn
*/
export function queue_idle_task(fn) {
if (!is_idle_task_queued) {
is_idle_task_queued = true;
request_idle_callback(process_idle_tasks);
export function queue_post_micro_task(fn) {
if (!is_micro_task_queued) {
is_micro_task_queued = true;
queueMicrotask(flush_all_micro_tasks);
}
current_queued_idle_tasks.push(fn);
queued_post_microtasks.push(fn);
}
/**
* Synchronously run any queued tasks.
* @param {() => void} fn
*/
export function flush_tasks() {
if (is_micro_task_queued) {
process_micro_tasks();
}
if (is_idle_task_queued) {
process_idle_tasks();
export function queue_idle_task(fn) {
if (!is_idle_task_queued) {
is_idle_task_queued = true;
request_idle_callback(flush_idle_tasks);
}
queued_idle_tasks.push(fn);
}

@ -528,15 +528,20 @@ export function unlink_effect(effect) {
* A paused effect does not update, and the DOM subtree becomes inert.
* @param {Effect} effect
* @param {() => void} [callback]
* @param {boolean} [destroy]
*/
export function pause_effect(effect, callback) {
export function pause_effect(effect, callback, destroy = true) {
/** @type {TransitionManager[]} */
var transitions = [];
pause_children(effect, transitions, true);
pause_children(effect, transitions, true, destroy);
run_out_transitions(transitions, () => {
destroy_effect(effect);
if (destroy) {
destroy_effect(effect);
} else {
execute_effect_teardown(effect);
}
if (callback) callback();
});
}
@ -561,8 +566,9 @@ export function run_out_transitions(transitions, fn) {
* @param {Effect} effect
* @param {TransitionManager[]} transitions
* @param {boolean} local
* @param {boolean} [destroy]
*/
export function pause_children(effect, transitions, local) {
export function pause_children(effect, transitions, local, destroy = true) {
if ((effect.f & INERT) !== 0) return;
effect.f ^= INERT;
@ -582,7 +588,7 @@ export function pause_children(effect, transitions, local) {
// TODO we don't need to call pause_children recursively with a linked list in place
// it's slightly more involved though as we have to account for `transparent` changing
// through the tree.
pause_children(child, transitions, transparent ? local : false);
pause_children(child, transitions, transparent ? local : false, destroy);
child = sibling;
}
}

@ -27,7 +27,11 @@ import {
DISCONNECTED,
BOUNDARY_EFFECT
} from './constants.js';
import { flush_tasks } from './dom/task.js';
import {
flush_idle_tasks,
flush_boundary_micro_tasks,
flush_post_micro_tasks
} from './dom/task.js';
import { add_owner } from './dev/ownership.js';
import { internal_set, set, source } from './reactivity/sources.js';
import { destroy_derived, execute_derived, update_derived } from './reactivity/deriveds.js';
@ -737,11 +741,12 @@ function flush_queued_effects(effects) {
}
}
function process_deferred() {
function flushed_deferred() {
is_micro_task_queued = false;
if (flush_count > 1001) {
return;
}
// flush_before_process_microtasks();
const previous_queued_root_effects = queued_root_effects;
queued_root_effects = [];
flush_queued_root_effects(previous_queued_root_effects);
@ -763,7 +768,7 @@ export function schedule_effect(signal) {
if (scheduler_mode === FLUSH_MICROTASK) {
if (!is_micro_task_queued) {
is_micro_task_queued = true;
queueMicrotask(process_deferred);
queueMicrotask(flushed_deferred);
}
}
@ -882,7 +887,9 @@ export function flush_sync(fn) {
var result = fn?.();
flush_tasks();
flush_boundary_micro_tasks();
flush_post_micro_tasks();
flush_idle_tasks();
if (queued_root_effects.length > 0 || root_effects.length > 0) {
flush_sync();
}

@ -1,6 +1,6 @@
import { flushSync } from 'svelte';
import { raf as svelte_raf } from 'svelte/internal/client';
import { queue_micro_task } from '../src/internal/client/dom/task.js';
import { queue_post_micro_task } from '../src/internal/client/dom/task.js';
export const raf = {
animations: new Set(),
@ -132,7 +132,7 @@ class Animation {
/** @param {() => {}} fn */
set onfinish(fn) {
if (this.#duration === 0) {
queue_micro_task(fn);
queue_post_micro_task(fn);
} else {
this.#onfinish = () => {
fn();

Loading…
Cancel
Save