diff --git a/.changeset/curvy-clouds-cut.md b/.changeset/curvy-clouds-cut.md new file mode 100644 index 000000000000..f980e513c612 --- /dev/null +++ b/.changeset/curvy-clouds-cut.md @@ -0,0 +1,5 @@ +--- +'svelte': patch +--- + +fix: don't execute attachments and attribute effects eagerly diff --git a/packages/svelte/src/internal/client/constants.js b/packages/svelte/src/internal/client/constants.js index b39afef51682..c8e7900a07b0 100644 --- a/packages/svelte/src/internal/client/constants.js +++ b/packages/svelte/src/internal/client/constants.js @@ -31,6 +31,11 @@ export const EAGER_EFFECT = 1 << 17; export const HEAD_EFFECT = 1 << 18; export const EFFECT_PRESERVED = 1 << 19; export const USER_EFFECT = 1 << 20; +/** + * A block effect that should run as part of render effects, i.e. not eagerly as part of tree traversal or effect flushing. + * Essentially it is a combination of RENDER_EFFECT and BLOCK_EFFECT. + */ +export const BLOCK_NON_EAGER = 1 << 24; // Flags exclusive to deriveds /** diff --git a/packages/svelte/src/internal/client/dom/elements/attachments.js b/packages/svelte/src/internal/client/dom/elements/attachments.js index 4fc128013888..1c145724b7b2 100644 --- a/packages/svelte/src/internal/client/dom/elements/attachments.js +++ b/packages/svelte/src/internal/client/dom/elements/attachments.js @@ -1,4 +1,5 @@ /** @import { Effect } from '#client' */ +import { BLOCK_NON_EAGER } from '#client/constants'; import { block, branch, effect, destroy_effect } from '../../reactivity/effects.js'; // TODO in 6.0 or 7.0, when we remove legacy mode, we can simplify this by @@ -29,5 +30,5 @@ export function attach(node, get_fn) { }); } } - }); + }, BLOCK_NON_EAGER); } diff --git a/packages/svelte/src/internal/client/dom/elements/attributes.js b/packages/svelte/src/internal/client/dom/elements/attributes.js index d970e7c885ca..21eda1bfead8 100644 --- a/packages/svelte/src/internal/client/dom/elements/attributes.js +++ b/packages/svelte/src/internal/client/dom/elements/attributes.js @@ -5,7 +5,7 @@ import { get_descriptors, get_prototype_of } from '../../../shared/utils.js'; import { create_event, delegate } from './events.js'; import { add_form_reset_listener, autofocus } from './misc.js'; import * as w from '../../warnings.js'; -import { LOADING_ATTR_SYMBOL } from '#client/constants'; +import { BLOCK_NON_EAGER, LOADING_ATTR_SYMBOL } from '#client/constants'; import { queue_micro_task } from '../task.js'; import { is_capture_event, can_delegate_event, normalize_attribute } from '../../../../utils.js'; import { @@ -540,7 +540,7 @@ export function attribute_effect( } prev = current; - }); + }, BLOCK_NON_EAGER); if (is_select) { var select = /** @type {HTMLSelectElement} */ (element); diff --git a/packages/svelte/src/internal/client/reactivity/batch.js b/packages/svelte/src/internal/client/reactivity/batch.js index 22526df7c1f2..526947e69ccb 100644 --- a/packages/svelte/src/internal/client/reactivity/batch.js +++ b/packages/svelte/src/internal/client/reactivity/batch.js @@ -17,7 +17,8 @@ import { EAGER_EFFECT, HEAD_EFFECT, ERROR_VALUE, - WAS_MARKED + WAS_MARKED, + BLOCK_NON_EAGER } from '#client/constants'; import { async_mode_flag } from '../../flags/index.js'; import { deferred, define_property } from '../../shared/utils.js'; @@ -234,7 +235,7 @@ export class Batch { effect.f ^= CLEAN; } else if ((flags & EFFECT) !== 0) { target.effects.push(effect); - } else if (async_mode_flag && (flags & RENDER_EFFECT) !== 0) { + } else if (async_mode_flag && (flags & (RENDER_EFFECT | BLOCK_NON_EAGER)) !== 0) { target.render_effects.push(effect); } else if (is_dirty(effect)) { if ((effect.f & BLOCK_EFFECT) !== 0) target.block_effects.push(effect); @@ -779,7 +780,7 @@ function mark_effects(value, sources, marked, checked) { mark_effects(/** @type {Derived} */ (reaction), sources, marked, checked); } else if ( (flags & (ASYNC | BLOCK_EFFECT)) !== 0 && - (flags & DIRTY) === 0 && // we may have scheduled this one already + (flags & (DIRTY | BLOCK_NON_EAGER)) === 0 && depends_on(reaction, sources, checked) ) { set_signal_status(reaction, DIRTY); @@ -855,7 +856,7 @@ export function schedule_effect(signal) { is_flushing && effect === active_effect && (flags & BLOCK_EFFECT) !== 0 && - (flags & HEAD_EFFECT) === 0 + (flags & (HEAD_EFFECT | BLOCK_NON_EAGER)) === 0 ) { return; } diff --git a/packages/svelte/src/internal/client/reactivity/sources.js b/packages/svelte/src/internal/client/reactivity/sources.js index 822fb218229a..c025603663c1 100644 --- a/packages/svelte/src/internal/client/reactivity/sources.js +++ b/packages/svelte/src/internal/client/reactivity/sources.js @@ -30,7 +30,8 @@ import { ROOT_EFFECT, ASYNC, WAS_MARKED, - CONNECTED + CONNECTED, + BLOCK_NON_EAGER } from '#client/constants'; import * as e from '../errors.js'; import { legacy_mode_flag, tracing_mode_flag } from '../../flags/index.js'; @@ -363,10 +364,12 @@ function mark_reactions(signal, status) { mark_reactions(derived, MAYBE_DIRTY); } } else if (not_dirty) { - if ((flags & BLOCK_EFFECT) !== 0) { - if (eager_block_effects !== null) { - eager_block_effects.add(/** @type {Effect} */ (reaction)); - } + if ( + (flags & BLOCK_EFFECT) !== 0 && + (flags & BLOCK_NON_EAGER) === 0 && + eager_block_effects !== null + ) { + eager_block_effects.add(/** @type {Effect} */ (reaction)); } schedule_effect(/** @type {Effect} */ (reaction)); diff --git a/packages/svelte/tests/runtime-runes/samples/async-fork-attributes/_config.js b/packages/svelte/tests/runtime-runes/samples/async-fork-attributes/_config.js new file mode 100644 index 000000000000..59bcdeb7f593 --- /dev/null +++ b/packages/svelte/tests/runtime-runes/samples/async-fork-attributes/_config.js @@ -0,0 +1,60 @@ +import { tick } from 'svelte'; +import { test } from '../../test'; + +export default test({ + async test({ assert, target }) { + const [fork, commit] = target.querySelectorAll('button'); + + fork.click(); + await tick(); + assert.htmlEqual( + target.innerHTML, + ` + + +

foo

+

foo

+

foo

+ ` + ); + + commit.click(); + await tick(); + assert.htmlEqual( + target.innerHTML, + ` + + +

foo

+

foo

+

foo

+ ` + ); + + fork.click(); + await tick(); + assert.htmlEqual( + target.innerHTML, + ` + + +

foo

+

foo

+

foo

+ ` + ); + + commit.click(); + await tick(); + assert.htmlEqual( + target.innerHTML, + ` + + +

foo

+

foo

+

foo

+ ` + ); + } +}); diff --git a/packages/svelte/tests/runtime-runes/samples/async-fork-attributes/main.svelte b/packages/svelte/tests/runtime-runes/samples/async-fork-attributes/main.svelte new file mode 100644 index 000000000000..956e5df6f3f2 --- /dev/null +++ b/packages/svelte/tests/runtime-runes/samples/async-fork-attributes/main.svelte @@ -0,0 +1,28 @@ + + + + + + + +

foo

+

foo

+

foo