From 87a061a73f574d4273de888f2924ba1e0ec4b956 Mon Sep 17 00:00:00 2001 From: Simon Holthausen Date: Tue, 22 Sep 2026 22:28:17 +0200 Subject: [PATCH] consolidate (maybe_)dirty_effects and dirty_deriveds --- .../internal/client/dom/blocks/boundary.js | 16 +-- .../src/internal/client/reactivity/batch.js | 105 +++++++----------- .../src/internal/client/reactivity/utils.js | 24 ++-- .../svelte/src/internal/client/runtime.js | 2 +- 4 files changed, 56 insertions(+), 91 deletions(-) diff --git a/packages/svelte/src/internal/client/dom/blocks/boundary.js b/packages/svelte/src/internal/client/dom/blocks/boundary.js index 5afe09adb4..2abfe0a9cc 100644 --- a/packages/svelte/src/internal/client/dom/blocks/boundary.js +++ b/packages/svelte/src/internal/client/dom/blocks/boundary.js @@ -1,4 +1,4 @@ -/** @import { Derived, Effect, Source, TemplateNode, } from '#client' */ +/** @import { Effect, Reaction, Source, TemplateNode, } from '#client' */ import { BOUNDARY_EFFECT, EFFECT_PRESERVED, EFFECT_TRANSPARENT } from '#client/constants'; import { HYDRATION_START_ELSE, HYDRATION_START_FAILED } from '../../../../constants.js'; import { component_context, set_component_context } from '../../context.js'; @@ -101,14 +101,8 @@ export class Boundary { #pending_count = 0; #pending_count_update_queued = false; - /** @type {Set} */ - #dirty_effects = new Set(); - - /** @type {Set} */ - #maybe_dirty_effects = new Set(); - - /** @type {Set} */ - #dirty_deriveds = new Set(); + /** @type {Map} */ + #dirty_reactions = new Map(); /** * A source containing the number of pending async deriveds/expressions. @@ -338,7 +332,7 @@ export class Boundary { // any effects that were previously deferred should be transferred // to the batch, which will flush in the next microtask - batch.transfer_effects(this.#dirty_effects, this.#maybe_dirty_effects, this.#dirty_deriveds); + batch.transfer_reactions(this.#dirty_reactions); } /** @@ -346,7 +340,7 @@ export class Boundary { * @param {Effect} effect */ defer_effect(effect) { - defer_effect(effect, this.#dirty_effects, this.#maybe_dirty_effects, this.#dirty_deriveds); + defer_effect(effect, this.#dirty_reactions); } /** diff --git a/packages/svelte/src/internal/client/reactivity/batch.js b/packages/svelte/src/internal/client/reactivity/batch.js index d8f491658c..b36fb826b1 100644 --- a/packages/svelte/src/internal/client/reactivity/batch.js +++ b/packages/svelte/src/internal/client/reactivity/batch.js @@ -242,27 +242,19 @@ export class Batch { #scheduled = []; /** - * Deferred effects (which run after async work has completed) that are DIRTY - * @type {Set} - */ - #dirty_effects = new Set(); - - /** - * Deferred effects that are MAYBE_DIRTY - * @type {Set} - */ - maybe_dirty_effects = new Set(); - - /** - * Deferred deriveds that are DIRTY. We need to store these because a derived that definitely should execute - * might get executed in the meantime in another batch (they are lazy, so a DIRTY derived is not guaranteed - * to run immediately). Relying on wv_values is insufficient because if this derived has stale dependencies - * in this batch but is executed with latest dependencies elsewhere, the wv is bumped and would incorrectly - * say "hey we don't need to rerun this" in the context of this batch. - * Lazily initialized for performance reasons. - * @type {Set | null} + * Deferred reactions and their status. + * + * Leaf (i.e. not block/async) effects (dirty and maybe_dirty) are stored because we need + * to reset their status when a batch becomes pending, to not pollute other batches. + * + * Dirty deriveds (but not maybe_dirty deriveds) are stored because a derived that definitely + * should execute might get executed in the meantime in another batch (they are lazy, so a DIRTY derived is + * not guaranteed to run immediately). Relying on wv_values is insufficient because if this derived has stale + * dependencies in this batch but is executed with latest dependencies elsewhere, the wv is bumped and would + * incorrectly say "hey we don't need to rerun this" in the context of this batch. + * @type {Map} */ - #dirty_deriveds = null; + #dirty_reactions = new Map(); /** * A map of branches that still exist, but will be destroyed when this batch @@ -429,22 +421,12 @@ export class Batch { // #is_deferred() is true, because traversing the tree could make // an if block that contains the last blocking pending effect falsy, // causing the block to no longer be deferred. - for (const e of this.#dirty_effects) { - this.maybe_dirty_effects.delete(e); - set_signal_status(e, DIRTY); - this.schedule(e); - } - - for (const e of this.maybe_dirty_effects) { - if ((e.f & DIRTY) === 0) { - set_signal_status(e, MAYBE_DIRTY); - this.schedule(e); - } - } - - if (this.#dirty_deriveds !== null) { - for (const d of this.#dirty_deriveds) { - set_signal_status(d, DIRTY); + for (const [reaction, status] of this.#dirty_reactions) { + if ((reaction.f & DERIVED) !== 0) { + set_signal_status(reaction, status); + } else if (status === DIRTY || (reaction.f & DIRTY) === 0) { + set_signal_status(reaction, status); + this.schedule(/** @type {Effect} */ (reaction)); } } @@ -527,8 +509,7 @@ export class Batch { } // clear effects. Those that are still needed will be rescheduled through unskipping the skipped branches. - this.#dirty_effects.clear(); - this.maybe_dirty_effects.clear(); + this.#dirty_reactions.clear(); this.apply(true); @@ -694,12 +675,12 @@ export class Batch { if ( not_yet - ? !this.seen_effects.has(effect) && - !this.#dirty_effects.has(effect) && - !this.maybe_dirty_effects.has(effect) + ? !this.seen_effects.has(effect) && !this.#dirty_reactions.has(effect) : (flags & (ASYNC | BLOCK_EFFECT)) === 0 || this.seen_effects.has(effect) ) { - this.maybe_dirty_effects.delete(effect); + if (this.#dirty_reactions.get(effect) === MAYBE_DIRTY) { + this.#dirty_reactions.delete(effect); + } set_signal_status(effect, status); this.schedule(effect); marked = true; @@ -774,7 +755,7 @@ export class Batch { // This can happen when batch Y merged into X and Y has a pending boundary and therefore still-pending async deriveds inside. batch.async_deriveds.clear(); - this.transfer_effects(batch.#dirty_effects, batch.maybe_dirty_effects, batch.#dirty_deriveds); + this.transfer_reactions(batch.#dirty_reactions); this.oncommit(() => batch.discard()); batch.#unlink(); @@ -789,12 +770,7 @@ export class Batch { */ #defer_effects(effects) { for (var i = 0; i < effects.length; i += 1) { - defer_effect( - effects[i], - this.#dirty_effects, - this.maybe_dirty_effects, - (this.#dirty_deriveds ??= new Set()) - ); + defer_effect(effects[i], this.#dirty_reactions); } } @@ -1053,32 +1029,29 @@ export class Batch { } /** - * @param {Set} dirty_effects - * @param {Set} maybe_dirty_effects - * @param {Set | null} dirty_deriveds + * @param {Map} dirty_reactions * @returns {void} */ - transfer_effects(dirty_effects, maybe_dirty_effects, dirty_deriveds) { + transfer_reactions(dirty_reactions) { if (this.merged_into) { - return this.merged_into.transfer_effects(dirty_effects, maybe_dirty_effects, dirty_deriveds); + return this.merged_into.transfer_reactions(dirty_reactions); } - for (const e of dirty_effects) { - this.#dirty_effects.add(e); + for (const [reaction, status] of dirty_reactions) { + this.add_dirty_reaction(reaction, status); } - for (const e of maybe_dirty_effects) { - this.maybe_dirty_effects.add(e); - } + dirty_reactions.clear(); + } - if (dirty_deriveds !== null) { - for (const d of dirty_deriveds) { - (this.#dirty_deriveds ??= new Set()).add(d); - } + /** + * @param {Reaction} reaction + * @param {number} status + */ + add_dirty_reaction(reaction, status) { + if (status === DIRTY || this.#dirty_reactions.get(reaction) !== DIRTY) { + this.#dirty_reactions.set(reaction, status); } - - dirty_effects.clear(); - maybe_dirty_effects.clear(); } /** @param {(batch: Batch) => void} fn */ diff --git a/packages/svelte/src/internal/client/reactivity/utils.js b/packages/svelte/src/internal/client/reactivity/utils.js index c03f463c54..0acf9f2cb4 100644 --- a/packages/svelte/src/internal/client/reactivity/utils.js +++ b/packages/svelte/src/internal/client/reactivity/utils.js @@ -1,18 +1,16 @@ -/** @import { Derived, Effect, Value } from '#client' */ +/** @import { Derived, Effect, Reaction, Value } from '#client' */ import { CLEAN, DERIVED, DIRTY, MAYBE_DIRTY } from '#client/constants'; import { set_signal_status } from './status.js'; /** * @param {Effect} effect - * @param {Set} dirty_effects - * @param {Set} maybe_dirty_effects - * @param {Set} dirty_deriveds + * @param {Map} dirty_reactions */ -export function defer_effect(effect, dirty_effects, maybe_dirty_effects, dirty_deriveds) { +export function defer_effect(effect, dirty_reactions) { if ((effect.f & DIRTY) !== 0) { - dirty_effects.add(effect); - } else if ((effect.f & MAYBE_DIRTY) !== 0) { - maybe_dirty_effects.add(effect); + dirty_reactions.set(effect, DIRTY); + } else if ((effect.f & MAYBE_DIRTY) !== 0 && dirty_reactions.get(effect) !== DIRTY) { + dirty_reactions.set(effect, MAYBE_DIRTY); } // mark as clean so they get scheduled if they depend on pending async state @@ -21,27 +19,27 @@ export function defer_effect(effect, dirty_effects, maybe_dirty_effects, dirty_d if (effect.deps === null) return; for (const dep of effect.deps) { - defer_derived(dep, dirty_deriveds); + defer_derived(dep, dirty_reactions); } } /** * @param {Value} value - * @param {Set} dirty_deriveds + * @param {Map} dirty_reactions */ -function defer_derived(value, dirty_deriveds) { +function defer_derived(value, dirty_reactions) { if ((value.f & DERIVED) === 0 || (value.f & CLEAN) !== 0) return; var derived = /** @type {Derived} */ (value); if ((derived.f & DIRTY) !== 0) { - dirty_deriveds.add(derived); + dirty_reactions.set(derived, DIRTY); set_signal_status(derived, MAYBE_DIRTY); } if (derived.deps === null) return; for (const dep of derived.deps) { - defer_derived(dep, dirty_deriveds); + defer_derived(dep, dirty_reactions); } } diff --git a/packages/svelte/src/internal/client/runtime.js b/packages/svelte/src/internal/client/runtime.js index 45fbd74afd..fefc70d785 100644 --- a/packages/svelte/src/internal/client/runtime.js +++ b/packages/svelte/src/internal/client/runtime.js @@ -516,7 +516,7 @@ export function update_effect(effect) { var own = /** @type {Batch} */ (own_batch); own.stale_effects.set(effect, write_version); for (var batch = own.next; batch !== null; batch = batch.next) { - batch.maybe_dirty_effects.add(effect); + batch.add_dirty_reaction(effect, MAYBE_DIRTY); } }