From 12235f0300fc5ddced002016e1c9d8af0128ecef Mon Sep 17 00:00:00 2001 From: Rich Harris Date: Tue, 24 Mar 2026 16:10:42 -0400 Subject: [PATCH] WIP --- .../src/internal/client/reactivity/batch.js | 67 ++++++++++++++++--- .../internal/client/reactivity/deriveds.js | 8 +-- .../src/internal/client/reactivity/effects.js | 1 - .../src/internal/client/reactivity/sources.js | 9 +-- .../src/internal/client/reactivity/types.d.ts | 4 +- .../svelte/src/internal/client/runtime.js | 42 +++++++----- 6 files changed, 92 insertions(+), 39 deletions(-) diff --git a/packages/svelte/src/internal/client/reactivity/batch.js b/packages/svelte/src/internal/client/reactivity/batch.js index 456d98b2f6..beef0b5fa3 100644 --- a/packages/svelte/src/internal/client/reactivity/batch.js +++ b/packages/svelte/src/internal/client/reactivity/batch.js @@ -26,13 +26,21 @@ import { get, increment_write_version, is_dirty, - update_effect + update_effect, + write_version } from '../runtime.js'; import * as e from '../errors.js'; import { flush_tasks, queue_micro_task } from '../dom/task.js'; import { DEV } from 'esm-env'; import { invoke_error_boundary } from '../error-handling.js'; -import { flush_eager_effects, old_values, set_eager_effects, source, update } from './sources.js'; +import { + flush_eager_effects, + mark_reactions, + old_values, + set_eager_effects, + source, + update +} from './sources.js'; import { eager_effect, unlink_effect } from './effects.js'; import { defer_effect } from './utils.js'; import { UNINITIALIZED } from '../../../constants.js'; @@ -60,6 +68,16 @@ export let previous_batch = null; */ export let batch_values = null; +/** + * @type {Map | null} + */ +export let batch_cvs = null; + +/** + * @type {Map | null} + */ +export let batch_wvs = null; + /** @type {Effect | null} */ let last_scheduled_effect = null; @@ -161,6 +179,12 @@ export class Batch { */ #maybe_dirty_effects = new Set(); + /** @type {Map} */ + wvs = new Map(); + + /** @type {Map} */ + cvs = new Map(); + /** * A map of branches that still exist, but will be destroyed when this batch * is committed — we skip over these during `process`. @@ -241,6 +265,10 @@ export class Batch { infinite_loop_guard(); } + for (const source of this.current.keys()) { + mark_reactions(source, null); + } + // we only reschedule previously-deferred effects if we expect // to be able to run them after processing the batch if (!this.#is_deferred()) { @@ -414,9 +442,8 @@ export class Batch { * batch, noting its previous and current values * @param {Value} source * @param {any} old_value - * @param {boolean} [is_derived] */ - capture(source, old_value, is_derived = false) { + capture(source, old_value) { if (old_value !== UNINITIALIZED && !this.previous.has(source)) { this.previous.set(source, old_value); } @@ -426,6 +453,11 @@ export class Batch { this.current.set(source, source.v); batch_values?.set(source, source.v); } + + var version = increment_write_version(); + + source.wv = version; + this.wvs.set(source, version); } /** @@ -450,7 +482,7 @@ export class Batch { deactivate() { current_batch = null; - batch_values = null; + batch_values = batch_cvs = batch_wvs = null; } flush() { @@ -469,7 +501,7 @@ export class Batch { is_processing = false; current_batch = null; - batch_values = null; + batch_values = batch_cvs = batch_wvs = null; old_values.clear(); @@ -672,7 +704,7 @@ export class Batch { apply() { if (!async_mode_flag) { // TODO previously we bailed here if there was only one (non-fork) batch... maybe we can reinstate that - batch_values = null; + batch_values = batch_cvs = batch_wvs = null; return; } @@ -683,6 +715,9 @@ export class Batch { batch_values.set(source, value); } + batch_cvs = this.cvs; + batch_wvs = this.wvs; + // ...and undo changes belonging to other batches unless they block this one for (const batch of batches) { if (batch === this || batch.is_fork) continue; @@ -717,6 +752,10 @@ export class Batch { schedule(effect) { last_scheduled_effect = effect; + if (!this.cvs.has(effect)) { + this.cvs.set(effect, effect.cv); + } + // defer render effects inside a pending boundary // TODO the `REACTION_RAN` check is only necessary because of legacy `$:` effects AFAICT — we can remove later if ( @@ -1036,12 +1075,16 @@ export function eager(fn) { // the first time this runs, we create an eager effect // that will run eagerly whenever the expression changes var previous_batch_values = batch_values; + var previous_batch_cvs = batch_cvs; + var previous_batch_wvs = batch_wvs; try { - batch_values = null; + batch_values = batch_cvs = batch_wvs = null; value = fn(); } finally { batch_values = previous_batch_values; + batch_cvs = previous_batch_cvs; + batch_wvs = previous_batch_wvs; } return; @@ -1198,6 +1241,14 @@ export function fork(fn) { }; } +/** + * @param {Reaction} reaction + */ +export function set_cv(reaction) { + batch_cvs?.set(reaction, write_version); + reaction.cv = write_version; +} + /** * Forcibly remove all current batches, to prevent cross-talk between tests */ diff --git a/packages/svelte/src/internal/client/reactivity/deriveds.js b/packages/svelte/src/internal/client/reactivity/deriveds.js index 42dfcb21ad..aa5f1b8aae 100644 --- a/packages/svelte/src/internal/client/reactivity/deriveds.js +++ b/packages/svelte/src/internal/client/reactivity/deriveds.js @@ -41,7 +41,7 @@ import { get_error } from '../../shared/dev.js'; import { async_mode_flag, tracing_mode_flag } from '../../flags/index.js'; import { component_context } from '../context.js'; import { UNINITIALIZED } from '../../../constants.js'; -import { batch_values, current_batch } from './batch.js'; +import { batch_values, batch_wvs, current_batch, set_cv } from './batch.js'; import { increment_pending, unset_context } from './async.js'; import { deferred, includes, noop } from '../../shared/utils.js'; @@ -120,7 +120,7 @@ export function async_derived(fn, label, location) { var promise = /** @type {Promise} */ (/** @type {unknown} */ (undefined)); var signal = source(/** @type {V} */ (UNINITIALIZED)); - if (DEV) signal.label = label; + if (DEV) signal.label = label ?? '{await ...}'; // only suspend in async deriveds created on initialisation var should_suspend = !active_reaction; @@ -385,12 +385,12 @@ export function execute_derived(derived) { * @returns {void} */ export function update_derived(derived) { - var old_value = derived.v; var value = execute_derived(derived); - derived.cv = write_version; + set_cv(derived); if (!derived.equals(value)) { + batch_wvs?.set(derived, write_version); derived.wv = write_version; // in a fork, we don't update the underlying value, just `batch_values`. diff --git a/packages/svelte/src/internal/client/reactivity/effects.js b/packages/svelte/src/internal/client/reactivity/effects.js index 7f77b0bf20..970cc9a640 100644 --- a/packages/svelte/src/internal/client/reactivity/effects.js +++ b/packages/svelte/src/internal/client/reactivity/effects.js @@ -111,7 +111,6 @@ function create_effect(type, fn) { b: parent && parent.b, prev: null, teardown: null, - wv: 0, cv: -1, ac: null }; diff --git a/packages/svelte/src/internal/client/reactivity/sources.js b/packages/svelte/src/internal/client/reactivity/sources.js index debfb2017a..b048f3c5fe 100644 --- a/packages/svelte/src/internal/client/reactivity/sources.js +++ b/packages/svelte/src/internal/client/reactivity/sources.js @@ -231,11 +231,9 @@ export function internal_set(source, value, updated_during_traversal = null) { } } - source.wv = increment_write_version(); - // For debugging, in case you want to know which reactions are being scheduled: // log_reactions(source); - mark_reactions(source, DIRTY, updated_during_traversal); + mark_reactions(source, updated_during_traversal); // It's possible that the current reaction might not have up-to-date dependencies // whilst it's actively running. So in the case of ensuring it registers the reaction @@ -314,11 +312,10 @@ export function increment(source) { /** * @param {Value} signal - * @param {number} status should be DIRTY or MAYBE_DIRTY * @param {Effect[] | null} updated_during_traversal * @returns {void} */ -function mark_reactions(signal, status, updated_during_traversal) { +export function mark_reactions(signal, updated_during_traversal) { var reactions = signal.reactions; if (reactions === null) return; @@ -349,7 +346,7 @@ function mark_reactions(signal, status, updated_during_traversal) { reaction.f |= WAS_MARKED; } - mark_reactions(derived, MAYBE_DIRTY, updated_during_traversal); + mark_reactions(derived, updated_during_traversal); } } else { var effect = /** @type {Effect} */ (reaction); diff --git a/packages/svelte/src/internal/client/reactivity/types.d.ts b/packages/svelte/src/internal/client/reactivity/types.d.ts index fbabf9d51f..fec23c69ff 100644 --- a/packages/svelte/src/internal/client/reactivity/types.d.ts +++ b/packages/svelte/src/internal/client/reactivity/types.d.ts @@ -11,8 +11,6 @@ import type { Boundary } from '../dom/blocks/boundary'; export interface Signal { /** Flags bitmask */ f: number; - /** Write version */ - wv: number; } export interface Value extends Signal { @@ -36,6 +34,8 @@ export interface Value extends Signal { set_during_effect?: boolean; /** A function that retrieves the underlying source, used for each block item signals */ trace?: null | (() => void); + /** Write version */ + wv: number; /** Read version */ rv: number; } diff --git a/packages/svelte/src/internal/client/runtime.js b/packages/svelte/src/internal/client/runtime.js index 49142b50ce..19c4fa1ebe 100644 --- a/packages/svelte/src/internal/client/runtime.js +++ b/packages/svelte/src/internal/client/runtime.js @@ -48,10 +48,13 @@ import { } from './context.js'; import { Batch, + batch_cvs, batch_values, + batch_wvs, current_batch, flushSync, - schedule_effect + schedule_effect, + set_cv } from './reactivity/batch.js'; import { handle_error } from './error-handling.js'; import { UNINITIALIZED } from '../../constants.js'; @@ -165,26 +168,31 @@ export function is_dirty(reaction) { var dependencies = /** @type {Value[]} */ (reaction.deps); - if (dependencies !== null) { - var length = dependencies.length; + if (dependencies === null) { + return false; + } - for (var i = 0; i < length; i++) { - var dependency = dependencies[i]; + var cv = batch_cvs?.get(reaction) ?? reaction.cv; - if ((dependency.f & DERIVED) !== 0) { - if (is_dirty(/** @type {Derived} */ (dependency))) { - update_derived(/** @type {Derived} */ (dependency)); - } - } + var length = dependencies.length; - if (dependency.wv > reaction.cv) { - return true; + for (var i = 0; i < length; i++) { + var dependency = dependencies[i]; + + if ((dependency.f & DERIVED) !== 0) { + if (is_dirty(/** @type {Derived} */ (dependency))) { + update_derived(/** @type {Derived} */ (dependency)); } } - } - reaction.cv = write_version; + var wv = batch_wvs?.get(dependency) ?? dependency.wv; + if (wv > cv) { + return true; + } + } + + set_cv(reaction); return false; } @@ -443,18 +451,16 @@ export function update_effect(effect) { destroy_effect_children(effect); } - effect.cv = write_version; + set_cv(effect); execute_effect_teardown(effect); var teardown = update_reaction(effect); effect.teardown = typeof teardown === 'function' ? teardown : null; if (!is_runes()) { - effect.cv = write_version; + set_cv(effect); } - effect.wv = write_version; // TODO should this be assigned before the update? - // In DEV, increment versions of any sources that were written to during the effect, // so that they are correctly marked as dirty when the effect re-runs if (DEV && tracing_mode_flag && (effect.f & DIRTY) !== 0 && effect.deps !== null) {