chore: simplify flushing (#15348)

* this appears to be unnecessary

* DRY out

* this doesnt appear to do anything useful

* simplify

* remove unused if block

* simplify, make non-recursive

* unused

* DRY

* simplify

* tidy up

* simplify

* changeset

* unused

* Revert "changeset"

This reverts commit 946e00dcf7.

* make flush_sync non-recursive

* fix flushSync types

* fix

* unused

* simplify

* tidy up

* tidy up

* present unnecessary microtasks, avoid flushing if no function provided

* simplify
pull/15380/head
Rich Harris 6 months ago committed by GitHub
parent 7958eb74df
commit 51337f22bd
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194

@ -0,0 +1,5 @@
---
'svelte': patch
---
fix: update types and inline docs for flushSync

@ -20,12 +20,12 @@ function setup() {
return {
destroy,
run() {
$.flush_sync(() => {
$.flush(() => {
$.set(head, 1);
});
assert($.get(computed5) === 6);
for (let i = 0; i < 1000; i++) {
$.flush_sync(() => {
$.flush(() => {
$.set(head, i);
});
assert($.get(computed5) === 6);

@ -25,12 +25,12 @@ function setup() {
return {
destroy,
run() {
$.flush_sync(() => {
$.flush(() => {
$.set(head, 1);
});
counter = 0;
for (let i = 0; i < 50; i++) {
$.flush_sync(() => {
$.flush(() => {
$.set(head, i);
});
assert($.get(last) === i + 50);

@ -25,12 +25,12 @@ function setup() {
return {
destroy,
run() {
$.flush_sync(() => {
$.flush(() => {
$.set(head, 1);
});
counter = 0;
for (let i = 0; i < iter; i++) {
$.flush_sync(() => {
$.flush(() => {
$.set(head, i);
});
assert($.get(current) === len + i);

@ -28,13 +28,13 @@ function setup() {
return {
destroy,
run() {
$.flush_sync(() => {
$.flush(() => {
$.set(head, 1);
});
assert($.get(sum) === 2 * width);
counter = 0;
for (let i = 0; i < 500; i++) {
$.flush_sync(() => {
$.flush(() => {
$.set(head, i);
});
assert($.get(sum) === (i + 1) * width);

@ -22,13 +22,13 @@ function setup() {
destroy,
run() {
for (let i = 0; i < 10; i++) {
$.flush_sync(() => {
$.flush(() => {
$.set(heads[i], i);
});
assert($.get(splited[i]) === i + 1);
}
for (let i = 0; i < 10; i++) {
$.flush_sync(() => {
$.flush(() => {
$.set(heads[i], i * 2);
});
assert($.get(splited[i]) === i * 2 + 1);

@ -25,13 +25,13 @@ function setup() {
return {
destroy,
run() {
$.flush_sync(() => {
$.flush(() => {
$.set(head, 1);
});
assert($.get(current) === size);
counter = 0;
for (let i = 0; i < 100; i++) {
$.flush_sync(() => {
$.flush(() => {
$.set(head, i);
});
assert($.get(current) === i * size);

@ -38,13 +38,13 @@ function setup() {
destroy,
run() {
const constant = count(width);
$.flush_sync(() => {
$.flush(() => {
$.set(head, 1);
});
assert($.get(sum) === constant);
counter = 0;
for (let i = 0; i < 100; i++) {
$.flush_sync(() => {
$.flush(() => {
$.set(head, i);
});
assert($.get(sum) === constant - width + i * width);

@ -25,13 +25,13 @@ function setup() {
return {
destroy,
run() {
$.flush_sync(() => {
$.flush(() => {
$.set(head, 1);
});
assert($.get(current) === 40);
counter = 0;
for (let i = 0; i < 100; i++) {
$.flush_sync(() => {
$.flush(() => {
$.set(head, i);
});
}

@ -51,11 +51,11 @@ function setup() {
*/
run(i) {
res.length = 0;
$.flush_sync(() => {
$.flush(() => {
$.set(B, 1);
$.set(A, 1 + i * 2);
});
$.flush_sync(() => {
$.flush(() => {
$.set(A, 2 + i * 2);
$.set(B, 2);
});

@ -312,7 +312,7 @@ export function client_component(analysis, options) {
const setter = b.set(key, [
b.stmt(b.call(b.id(name), b.id('$$value'))),
b.stmt(b.call('$.flush_sync'))
b.stmt(b.call('$.flush'))
]);
if (analysis.runes && binding.initial) {

@ -1,7 +1,7 @@
/** @import { ComponentContext, ComponentContextLegacy } from '#client' */
/** @import { EventDispatcher } from './index.js' */
/** @import { NotFunction } from './internal/types.js' */
import { flush_sync, untrack } from './internal/client/runtime.js';
import { untrack } from './internal/client/runtime.js';
import { is_array } from './internal/shared/utils.js';
import { user_effect } from './internal/client/index.js';
import * as e from './internal/client/errors.js';
@ -206,15 +206,7 @@ function init_update_callbacks(context) {
return (l.u ??= { a: [], b: [], m: [] });
}
/**
* Synchronously flushes any pending state changes and those that result from it.
* @param {() => void} [fn]
* @returns {void}
*/
export function flushSync(fn) {
flush_sync(fn);
}
export { flushSync } from './internal/client/runtime.js';
export { getContext, getAllContexts, hasContext, setContext } from './internal/client/context.js';
export { hydrate, mount, unmount } from './internal/client/render.js';
export { tick, untrack } from './internal/client/runtime.js';

@ -3,7 +3,7 @@ import { DEV } from 'esm-env';
import { is_promise } from '../../../shared/utils.js';
import { block, branch, pause_effect, resume_effect } from '../../reactivity/effects.js';
import { internal_set, mutable_source, source } from '../../reactivity/sources.js';
import { flush_sync, set_active_effect, set_active_reaction } from '../../runtime.js';
import { flushSync, set_active_effect, set_active_reaction } from '../../runtime.js';
import { hydrate_next, hydrate_node, hydrating } from '../hydration.js';
import { queue_micro_task } from '../task.js';
import { UNINITIALIZED } from '../../../../constants.js';
@ -105,7 +105,7 @@ export function await_block(node, get_input, pending_fn, then_fn, catch_fn) {
// without this, the DOM does not update until two ticks after the promise
// resolves, which is unexpected behaviour (and somewhat irksome to test)
flush_sync();
flushSync();
}
}
}

@ -6,25 +6,21 @@ const request_idle_callback =
? (/** @type {() => void} */ cb) => setTimeout(cb, 1)
: requestIdleCallback;
let is_micro_task_queued = false;
let is_idle_task_queued = false;
/** @type {Array<() => void>} */
let current_queued_micro_tasks = [];
let micro_tasks = [];
/** @type {Array<() => void>} */
let current_queued_idle_tasks = [];
let idle_tasks = [];
function process_micro_tasks() {
is_micro_task_queued = false;
const tasks = current_queued_micro_tasks.slice();
current_queued_micro_tasks = [];
function run_micro_tasks() {
var tasks = micro_tasks;
micro_tasks = [];
run_all(tasks);
}
function process_idle_tasks() {
is_idle_task_queued = false;
const tasks = current_queued_idle_tasks.slice();
current_queued_idle_tasks = [];
function run_idle_tasks() {
var tasks = idle_tasks;
idle_tasks = [];
run_all(tasks);
}
@ -32,32 +28,33 @@ function process_idle_tasks() {
* @param {() => void} fn
*/
export function queue_micro_task(fn) {
if (!is_micro_task_queued) {
is_micro_task_queued = true;
queueMicrotask(process_micro_tasks);
if (micro_tasks.length === 0) {
queueMicrotask(run_micro_tasks);
}
current_queued_micro_tasks.push(fn);
micro_tasks.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);
if (idle_tasks.length === 0) {
request_idle_callback(run_idle_tasks);
}
current_queued_idle_tasks.push(fn);
idle_tasks.push(fn);
}
/**
* Synchronously run any queued tasks.
*/
export function flush_tasks() {
if (is_micro_task_queued) {
process_micro_tasks();
if (micro_tasks.length > 0) {
run_micro_tasks();
}
if (is_idle_task_queued) {
process_idle_tasks();
if (idle_tasks.length > 0) {
run_idle_tasks();
}
}

@ -139,7 +139,7 @@ export {
get,
safe_get,
invalidate_inner_signals,
flush_sync,
flushSync as flush,
tick,
untrack,
exclude_from_object,

@ -6,15 +6,12 @@ import {
update_effect,
get,
is_destroying_effect,
is_flushing_effect,
remove_reactions,
schedule_effect,
set_active_reaction,
set_is_destroying_effect,
set_is_flushing_effect,
set_signal_status,
untrack,
skip_reaction,
untracking
} from '../runtime.js';
import {
@ -118,17 +115,12 @@ function create_effect(type, fn, sync, push = true) {
}
if (sync) {
var previously_flushing_effect = is_flushing_effect;
try {
set_is_flushing_effect(true);
update_effect(effect);
effect.f |= EFFECT_RAN;
} catch (e) {
destroy_effect(effect);
throw e;
} finally {
set_is_flushing_effect(previously_flushing_effect);
}
} else if (fn !== null) {
schedule_effect(effect);

@ -14,8 +14,6 @@ import {
derived_sources,
set_derived_sources,
check_dirtiness,
set_is_flushing_effect,
is_flushing_effect,
untracking
} from '../runtime.js';
import { equals, safe_equals } from './equality.js';
@ -202,22 +200,18 @@ export function internal_set(source, value) {
if (DEV && inspect_effects.size > 0) {
const inspects = Array.from(inspect_effects);
var previously_flushing_effect = is_flushing_effect;
set_is_flushing_effect(true);
try {
for (const effect of inspects) {
// Mark clean inspect-effects as maybe dirty and then check their dirtiness
// instead of just updating the effects - this way we avoid overfiring.
if ((effect.f & CLEAN) !== 0) {
set_signal_status(effect, MAYBE_DIRTY);
}
if (check_dirtiness(effect)) {
update_effect(effect);
}
for (const effect of inspects) {
// Mark clean inspect-effects as maybe dirty and then check their dirtiness
// instead of just updating the effects - this way we avoid overfiring.
if ((effect.f & CLEAN) !== 0) {
set_signal_status(effect, MAYBE_DIRTY);
}
if (check_dirtiness(effect)) {
update_effect(effect);
}
} finally {
set_is_flushing_effect(previously_flushing_effect);
}
inspect_effects.clear();
}
}

@ -9,7 +9,6 @@ import {
} from './reactivity/effects.js';
import {
EFFECT,
RENDER_EFFECT,
DIRTY,
MAYBE_DIRTY,
CLEAN,
@ -41,28 +40,19 @@ import {
} from './context.js';
import { is_firefox } from './dom/operations.js';
const FLUSH_MICROTASK = 0;
const FLUSH_SYNC = 1;
// Used for DEV time error handling
/** @param {WeakSet<Error>} value */
const handled_errors = new WeakSet();
let is_throwing_error = false;
// Used for controlling the flush of effects.
let scheduler_mode = FLUSH_MICROTASK;
// Used for handling scheduling
let is_micro_task_queued = false;
let is_flushing = false;
/** @type {Effect | null} */
let last_scheduled_effect = null;
export let is_flushing_effect = false;
export let is_destroying_effect = false;
let is_updating_effect = false;
/** @param {boolean} value */
export function set_is_flushing_effect(value) {
is_flushing_effect = value;
}
export let is_destroying_effect = false;
/** @param {boolean} value */
export function set_is_destroying_effect(value) {
@ -74,7 +64,6 @@ export function set_is_destroying_effect(value) {
/** @type {Effect[]} */
let queued_root_effects = [];
let flush_count = 0;
/** @type {Effect[]} Stack of effects, dev only */
let dev_effect_stack = [];
// Handle signal reactivity tree dependencies and reactions
@ -410,10 +399,9 @@ export function update_reaction(reaction) {
new_deps = /** @type {null | Value[]} */ (null);
skipped_deps = 0;
untracked_writes = null;
active_reaction = (flags & (BRANCH_EFFECT | ROOT_EFFECT)) === 0 ? reaction : null;
skip_reaction =
(flags & UNOWNED) !== 0 &&
(!is_flushing_effect || previous_reaction === null || previous_untracking);
(flags & UNOWNED) !== 0 && (untracking || !is_updating_effect || active_reaction === null);
active_reaction = (flags & (BRANCH_EFFECT | ROOT_EFFECT)) === 0 ? reaction : null;
derived_sources = null;
set_component_context(reaction.ctx);
@ -559,8 +547,10 @@ export function update_effect(effect) {
var previous_effect = active_effect;
var previous_component_context = component_context;
var was_updating_effect = is_updating_effect;
active_effect = effect;
is_updating_effect = true;
if (DEV) {
var previous_component_fn = dev_current_component_function;
@ -602,6 +592,7 @@ export function update_effect(effect) {
} catch (error) {
handle_error(error, effect, previous_effect, previous_component_context || effect.ctx);
} finally {
is_updating_effect = was_updating_effect;
active_effect = previous_effect;
if (DEV) {
@ -620,69 +611,70 @@ function log_effect_stack() {
}
function infinite_loop_guard() {
if (flush_count > 1000) {
flush_count = 0;
try {
e.effect_update_depth_exceeded();
} catch (error) {
try {
e.effect_update_depth_exceeded();
} catch (error) {
if (DEV) {
// stack is garbage, ignore. Instead add a console.error message.
define_property(error, 'stack', {
value: ''
});
}
// Try and handle the error so it can be caught at a boundary, that's
// if there's an effect available from when it was last scheduled
if (last_scheduled_effect !== null) {
if (DEV) {
// stack is garbage, ignore. Instead add a console.error message.
define_property(error, 'stack', {
value: ''
});
}
// Try and handle the error so it can be caught at a boundary, that's
// if there's an effect available from when it was last scheduled
if (last_scheduled_effect !== null) {
if (DEV) {
try {
handle_error(error, last_scheduled_effect, null, null);
} catch (e) {
// Only log the effect stack if the error is re-thrown
log_effect_stack();
throw e;
}
} else {
try {
handle_error(error, last_scheduled_effect, null, null);
}
} else {
if (DEV) {
} catch (e) {
// Only log the effect stack if the error is re-thrown
log_effect_stack();
throw e;
}
throw error;
} else {
handle_error(error, last_scheduled_effect, null, null);
}
} else {
if (DEV) {
log_effect_stack();
}
throw error;
}
}
flush_count++;
}
/**
* @param {Array<Effect>} root_effects
* @returns {void}
*/
function flush_queued_root_effects(root_effects) {
var length = root_effects.length;
if (length === 0) {
return;
}
infinite_loop_guard();
var previously_flushing_effect = is_flushing_effect;
is_flushing_effect = true;
function flush_queued_root_effects() {
try {
for (var i = 0; i < length; i++) {
var effect = root_effects[i];
var flush_count = 0;
if ((effect.f & CLEAN) === 0) {
effect.f ^= CLEAN;
while (queued_root_effects.length > 0) {
if (flush_count++ > 1000) {
infinite_loop_guard();
}
var collected_effects = process_effects(effect);
flush_queued_effects(collected_effects);
var root_effects = queued_root_effects;
var length = root_effects.length;
queued_root_effects = [];
for (var i = 0; i < length; i++) {
var root = root_effects[i];
if ((root.f & CLEAN) === 0) {
root.f ^= CLEAN;
}
var collected_effects = process_effects(root);
flush_queued_effects(collected_effects);
}
}
} finally {
is_flushing_effect = previously_flushing_effect;
is_flushing = false;
last_scheduled_effect = null;
if (DEV) {
dev_effect_stack = [];
}
}
}
@ -724,39 +716,17 @@ function flush_queued_effects(effects) {
}
}
function process_deferred() {
is_micro_task_queued = false;
if (flush_count > 1001) {
return;
}
const previous_queued_root_effects = queued_root_effects;
queued_root_effects = [];
flush_queued_root_effects(previous_queued_root_effects);
if (!is_micro_task_queued) {
flush_count = 0;
last_scheduled_effect = null;
if (DEV) {
dev_effect_stack = [];
}
}
}
/**
* @param {Effect} signal
* @returns {void}
*/
export function schedule_effect(signal) {
if (scheduler_mode === FLUSH_MICROTASK) {
if (!is_micro_task_queued) {
is_micro_task_queued = true;
queueMicrotask(process_deferred);
}
if (!is_flushing) {
is_flushing = true;
queueMicrotask(flush_queued_root_effects);
}
last_scheduled_effect = signal;
var effect = signal;
var effect = (last_scheduled_effect = signal);
while (effect.parent !== null) {
effect = effect.parent;
@ -846,42 +816,30 @@ function process_effects(effect) {
}
/**
* Internal version of `flushSync` with the option to not flush previous effects.
* Returns the result of the passed function, if given.
* @param {() => any} [fn]
* @returns {any}
* Synchronously flush any pending updates.
* Returns void if no callback is provided, otherwise returns the result of calling the callback.
* @template [T=void]
* @param {(() => T) | undefined} [fn]
* @returns {T}
*/
export function flush_sync(fn) {
var previous_scheduler_mode = scheduler_mode;
var previous_queued_root_effects = queued_root_effects;
try {
infinite_loop_guard();
export function flushSync(fn) {
var result;
scheduler_mode = FLUSH_SYNC;
queued_root_effects = [];
is_micro_task_queued = false;
flush_queued_root_effects(previous_queued_root_effects);
if (fn) {
is_flushing = true;
flush_queued_root_effects();
result = fn();
}
var result = fn?.();
flush_tasks();
while (queued_root_effects.length > 0) {
is_flushing = true;
flush_queued_root_effects();
flush_tasks();
if (queued_root_effects.length > 0) {
flush_sync();
}
flush_count = 0;
last_scheduled_effect = null;
if (DEV) {
dev_effect_stack = [];
}
return result;
} finally {
scheduler_mode = previous_scheduler_mode;
queued_root_effects = previous_queued_root_effects;
}
return /** @type {T} */ (result);
}
/**
@ -890,9 +848,9 @@ export function flush_sync(fn) {
*/
export async function tick() {
await Promise.resolve();
// By calling flush_sync we guarantee that any pending state changes are applied after one tick.
// By calling flushSync we guarantee that any pending state changes are applied after one tick.
// TODO look into whether we can make flushing subsequent updates synchronously in the future.
flush_sync();
flushSync();
}
/**

@ -3,7 +3,7 @@ import { DIRTY, LEGACY_PROPS, MAYBE_DIRTY } from '../internal/client/constants.j
import { user_pre_effect } from '../internal/client/reactivity/effects.js';
import { mutable_source, set } from '../internal/client/reactivity/sources.js';
import { hydrate, mount, unmount } from '../internal/client/render.js';
import { active_effect, flush_sync, get, set_signal_status } from '../internal/client/runtime.js';
import { active_effect, flushSync, get, set_signal_status } from '../internal/client/runtime.js';
import { lifecycle_outside_component } from '../internal/shared/errors.js';
import { define_property, is_array } from '../internal/shared/utils.js';
import * as w from '../internal/client/warnings.js';
@ -119,9 +119,9 @@ class Svelte4Component {
recover: options.recover
});
// We don't flush_sync for custom element wrappers or if the user doesn't want it
// We don't flushSync for custom element wrappers or if the user doesn't want it
if (!options?.props?.$$host || options.sync === false) {
flush_sync();
flushSync();
}
this.#events = props.$$events;

@ -47,6 +47,8 @@ export default test({
{ id: 1, name: 'a' }
];
raf.tick(0);
divs = target.querySelectorAll('div');
assert.ok(divs[0].getAnimations().length > 0);
assert.equal(divs[1].getAnimations().length, 0);

@ -46,6 +46,8 @@ export default test({
{ id: 1, name: 'a' }
];
raf.tick(0);
divs = document.querySelectorAll('div');
assert.equal(divs[0].dy, 120);
assert.equal(divs[4].dy, -120);

@ -46,6 +46,8 @@ export default test({
{ id: 1, name: 'a' }
];
raf.tick(0);
divs = document.querySelectorAll('div');
assert.equal(divs[0].dy, 120);
assert.equal(divs[4].dy, -120);
@ -66,6 +68,8 @@ export default test({
{ id: 5, name: 'e' }
];
raf.tick(100);
divs = document.querySelectorAll('div');
assert.equal(divs[0].dy, 120);

@ -50,6 +50,8 @@ export default test({
{ id: 1, name: 'a' }
];
raf.tick(0);
divs = target.querySelectorAll('div');
assert.equal(divs[0].style.transform, 'translate(0px, 120px)');
assert.equal(divs[1].style.transform, '');

@ -602,7 +602,7 @@ describe('toStore', () => {
assert.deepEqual(log, [0]);
set(count, 1);
$.flush_sync();
$.flushSync();
assert.deepEqual(log, [0, 1]);
unsubscribe();
@ -625,7 +625,7 @@ describe('toStore', () => {
assert.deepEqual(log, [0]);
set(count, 1);
$.flush_sync();
$.flushSync();
assert.deepEqual(log, [0, 1]);
store.set(2);
@ -654,11 +654,11 @@ describe('fromStore', () => {
assert.deepEqual(log, [0]);
store.set(1);
$.flush_sync();
$.flushSync();
assert.deepEqual(log, [0, 1]);
count.current = 2;
$.flush_sync();
$.flushSync();
assert.deepEqual(log, [0, 1, 2]);
assert.equal(get(store), 2);

@ -408,10 +408,6 @@ declare module 'svelte' {
* @deprecated Use [`$effect`](https://svelte.dev/docs/svelte/$effect) instead
* */
export function afterUpdate(fn: () => void): void;
/**
* Synchronously flushes any pending state changes and those that result from it.
* */
export function flushSync(fn?: (() => void) | undefined): void;
/**
* Create a snippet programmatically
* */
@ -421,6 +417,29 @@ declare module 'svelte' {
}): Snippet<Params>;
/** Anything except a function */
type NotFunction<T> = T extends Function ? never : T;
/**
* Synchronously flush any pending updates.
* Returns void if no callback is provided, otherwise returns the result of calling the callback.
* */
export function flushSync<T = void>(fn?: (() => T) | undefined): T;
/**
* Returns a promise that resolves once any pending state changes have been applied.
* */
export function tick(): Promise<void>;
/**
* When used inside a [`$derived`](https://svelte.dev/docs/svelte/$derived) or [`$effect`](https://svelte.dev/docs/svelte/$effect),
* any state read inside `fn` will not be treated as a dependency.
*
* ```ts
* $effect(() => {
* // this will run when `data` changes, but not when `time` changes
* save(data, {
* timestamp: untrack(() => time)
* });
* });
* ```
* */
export function untrack<T>(fn: () => T): T;
/**
* Retrieves the context that belongs to the closest parent component with the specified `key`.
* Must be called during component initialisation.
@ -494,24 +513,6 @@ declare module 'svelte' {
export function unmount(component: Record<string, any>, options?: {
outro?: boolean;
} | undefined): Promise<void>;
/**
* Returns a promise that resolves once any pending state changes have been applied.
* */
export function tick(): Promise<void>;
/**
* When used inside a [`$derived`](https://svelte.dev/docs/svelte/$derived) or [`$effect`](https://svelte.dev/docs/svelte/$effect),
* any state read inside `fn` will not be treated as a dependency.
*
* ```ts
* $effect(() => {
* // this will run when `data` changes, but not when `time` changes
* save(data, {
* timestamp: untrack(() => time)
* });
* });
* ```
* */
export function untrack<T>(fn: () => T): T;
type Getters<T> = {
[K in keyof T]: () => T[K];
};

Loading…
Cancel
Save