diff --git a/packages/svelte/src/internal/client/reactivity/batch.js b/packages/svelte/src/internal/client/reactivity/batch.js index 52def2a715..f1f20164e3 100644 --- a/packages/svelte/src/internal/client/reactivity/batch.js +++ b/packages/svelte/src/internal/client/reactivity/batch.js @@ -10,7 +10,8 @@ import { INERT, RENDER_EFFECT, ROOT_EFFECT, - USER_EFFECT + USER_EFFECT, + MAYBE_DIRTY } from '#client/constants'; import { async_mode_flag } from '../../flags/index.js'; import { deferred, define_property } from '../../shared/utils.js'; @@ -146,6 +147,18 @@ export class Batch { */ #block_effects = []; + /** + * Deferred effects (which run after async work has completed) that are DIRTY + * @type {Effect[]} + */ + #dirty_effects = []; + + /** + * Deferred effects that are MAYBE_DIRTY + * @type {Effect[]} + */ + #maybe_dirty_effects = []; + /** * A set of branches that still exist, but will be destroyed when this batch * is committed — we skip over these during `process` @@ -221,10 +234,9 @@ export class Batch { this.#deferred?.resolve(); } else { - // otherwise mark effects clean so they get scheduled on the next run - for (const e of this.#render_effects) set_signal_status(e, CLEAN); - for (const e of this.#effects) set_signal_status(e, CLEAN); - for (const e of this.#block_effects) set_signal_status(e, CLEAN); + this.#defer_effects(this.#render_effects); + this.#defer_effects(this.#effects); + this.#defer_effects(this.#block_effects); } if (current_values) { @@ -307,6 +319,21 @@ export class Batch { } } + /** + * @param {Effect[]} effects + */ + #defer_effects(effects) { + for (const e of effects) { + const target = (e.f & DIRTY) !== 0 ? this.#dirty_effects : this.#maybe_dirty_effects; + target.push(e); + + // mark as clean so they get scheduled if they depend on pending async state + set_signal_status(e, CLEAN); + } + + effects.length = 0; + } + /** * Associate a change to a given source with the current * batch, noting its previous and current values @@ -384,18 +411,13 @@ export class Batch { this.#pending -= 1; if (this.#pending === 0) { - for (const e of this.#render_effects) { + for (const e of this.#dirty_effects) { set_signal_status(e, DIRTY); schedule_effect(e); } - for (const e of this.#effects) { - set_signal_status(e, DIRTY); - schedule_effect(e); - } - - for (const e of this.#block_effects) { - set_signal_status(e, DIRTY); + for (const e of this.#maybe_dirty_effects) { + set_signal_status(e, MAYBE_DIRTY); schedule_effect(e); } diff --git a/packages/svelte/tests/runtime-runes/samples/async-effect-conservative/_config.js b/packages/svelte/tests/runtime-runes/samples/async-effect-conservative/_config.js new file mode 100644 index 0000000000..bab06a203d --- /dev/null +++ b/packages/svelte/tests/runtime-runes/samples/async-effect-conservative/_config.js @@ -0,0 +1,28 @@ +import { tick } from 'svelte'; +import { test } from '../../test'; + +export default test({ + async test({ assert, target, logs }) { + await tick(); + + const [increment] = target.querySelectorAll('button'); + + assert.deepEqual(logs, [false]); + assert.htmlEqual(target.innerHTML, '

0

'); + + increment.click(); + await tick(); + assert.deepEqual(logs, [false]); + assert.htmlEqual(target.innerHTML, '

1

'); + + increment.click(); + await tick(); + assert.deepEqual(logs, [false, true]); + assert.htmlEqual(target.innerHTML, '

2

'); + + increment.click(); + await tick(); + assert.deepEqual(logs, [false, true]); + assert.htmlEqual(target.innerHTML, '

3

'); + } +}); diff --git a/packages/svelte/tests/runtime-runes/samples/async-effect-conservative/main.svelte b/packages/svelte/tests/runtime-runes/samples/async-effect-conservative/main.svelte new file mode 100644 index 0000000000..5305067a5a --- /dev/null +++ b/packages/svelte/tests/runtime-runes/samples/async-effect-conservative/main.svelte @@ -0,0 +1,17 @@ + + + + +

{await count}

+ + {#snippet pending()} +

loading...

+ {/snippet} +