Skip to content

Commit 53bbe34

Browse files
authored
fix: don't execute attachments and attribute effects eagerly (#17208)
* fix: don't execute attachments and attribute effects eagerly attributes_effect and attachments are blocks since they need the managed "don't just destroy children effects"-behavior, but they're not block effects in the sense of "run them eagerly while traversing the effect tree or while flushing effects". Since the latter was the case until now, it meant that forks could cause visible UI updates. This PR introduces a new flag to fix that. `BLOCK_NON_EAGER` is basically a combination of block effects (with respects to the managed behavior) and render effects (with respects to the execution timing). Fixes sveltejs/kit#14931 * managed_effect
1 parent 1aafbc4 commit 53bbe34

File tree

10 files changed

+129
-14
lines changed

10 files changed

+129
-14
lines changed

.changeset/curvy-clouds-cut.md

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
---
2+
'svelte': patch
3+
---
4+
5+
fix: don't execute attachments and attribute effects eagerly

packages/svelte/src/internal/client/constants.js

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,15 @@
22
export const DERIVED = 1 << 1;
33
export const EFFECT = 1 << 2;
44
export const RENDER_EFFECT = 1 << 3;
5+
/**
6+
* An effect that does not destroy its child effects when it reruns.
7+
* Runs as part of render effects, i.e. not eagerly as part of tree traversal or effect flushing.
8+
*/
9+
export const MANAGED_EFFECT = 1 << 24;
10+
/**
11+
* An effect that does not destroy its child effects when it reruns (like MANAGED_EFFECT).
12+
* Runs eagerly as part of tree traversal or effect flushing.
13+
*/
514
export const BLOCK_EFFECT = 1 << 4;
615
export const BRANCH_EFFECT = 1 << 5;
716
export const ROOT_EFFECT = 1 << 6;

packages/svelte/src/internal/client/dom/elements/attachments.js

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
/** @import { Effect } from '#client' */
2-
import { block, branch, effect, destroy_effect } from '../../reactivity/effects.js';
2+
import { branch, effect, destroy_effect, managed } from '../../reactivity/effects.js';
33

44
// TODO in 6.0 or 7.0, when we remove legacy mode, we can simplify this by
55
// getting rid of the block/branch stuff and just letting the effect rip.
@@ -16,7 +16,7 @@ export function attach(node, get_fn) {
1616
/** @type {Effect | null} */
1717
var e;
1818

19-
block(() => {
19+
managed(() => {
2020
if (fn !== (fn = get_fn())) {
2121
if (e) {
2222
destroy_effect(e);

packages/svelte/src/internal/client/dom/elements/attributes.js

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -20,7 +20,7 @@ import { clsx } from '../../../shared/attributes.js';
2020
import { set_class } from './class.js';
2121
import { set_style } from './style.js';
2222
import { ATTACHMENT_KEY, NAMESPACE_HTML, UNINITIALIZED } from '../../../../constants.js';
23-
import { block, branch, destroy_effect, effect } from '../../reactivity/effects.js';
23+
import { branch, destroy_effect, effect, managed } from '../../reactivity/effects.js';
2424
import { init_select, select_option } from './bindings/select.js';
2525
import { flatten } from '../../reactivity/async.js';
2626

@@ -508,7 +508,7 @@ export function attribute_effect(
508508
var is_select = element.nodeName === 'SELECT';
509509
var inited = false;
510510

511-
block(() => {
511+
managed(() => {
512512
var next = fn(...values.map(get));
513513
/** @type {Record<string | symbol, any>} */
514514
var current = set_attributes(

packages/svelte/src/internal/client/reactivity/batch.js

Lines changed: 4 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -17,7 +17,8 @@ import {
1717
EAGER_EFFECT,
1818
HEAD_EFFECT,
1919
ERROR_VALUE,
20-
WAS_MARKED
20+
WAS_MARKED,
21+
MANAGED_EFFECT
2122
} from '#client/constants';
2223
import { async_mode_flag } from '../../flags/index.js';
2324
import { deferred, define_property } from '../../shared/utils.js';
@@ -234,7 +235,7 @@ export class Batch {
234235
effect.f ^= CLEAN;
235236
} else if ((flags & EFFECT) !== 0) {
236237
target.effects.push(effect);
237-
} else if (async_mode_flag && (flags & RENDER_EFFECT) !== 0) {
238+
} else if (async_mode_flag && (flags & (RENDER_EFFECT | MANAGED_EFFECT)) !== 0) {
238239
target.render_effects.push(effect);
239240
} else if (is_dirty(effect)) {
240241
if ((effect.f & BLOCK_EFFECT) !== 0) target.block_effects.push(effect);
@@ -779,7 +780,7 @@ function mark_effects(value, sources, marked, checked) {
779780
mark_effects(/** @type {Derived} */ (reaction), sources, marked, checked);
780781
} else if (
781782
(flags & (ASYNC | BLOCK_EFFECT)) !== 0 &&
782-
(flags & DIRTY) === 0 && // we may have scheduled this one already
783+
(flags & DIRTY) === 0 &&
783784
depends_on(reaction, sources, checked)
784785
) {
785786
set_signal_status(reaction, DIRTY);

packages/svelte/src/internal/client/reactivity/effects.js

Lines changed: 14 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -33,7 +33,8 @@ import {
3333
STALE_REACTION,
3434
USER_EFFECT,
3535
ASYNC,
36-
CONNECTED
36+
CONNECTED,
37+
MANAGED_EFFECT
3738
} from '#client/constants';
3839
import * as e from '../errors.js';
3940
import { DEV } from 'esm-env';
@@ -401,6 +402,18 @@ export function block(fn, flags = 0) {
401402
return effect;
402403
}
403404

405+
/**
406+
* @param {(() => void)} fn
407+
* @param {number} flags
408+
*/
409+
export function managed(fn, flags = 0) {
410+
var effect = create_effect(MANAGED_EFFECT | flags, fn, true);
411+
if (DEV) {
412+
effect.dev_stack = dev_stack;
413+
}
414+
return effect;
415+
}
416+
404417
/**
405418
* @param {(() => void)} fn
406419
*/

packages/svelte/src/internal/client/reactivity/sources.js

Lines changed: 2 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -363,10 +363,8 @@ function mark_reactions(signal, status) {
363363
mark_reactions(derived, MAYBE_DIRTY);
364364
}
365365
} else if (not_dirty) {
366-
if ((flags & BLOCK_EFFECT) !== 0) {
367-
if (eager_block_effects !== null) {
368-
eager_block_effects.add(/** @type {Effect} */ (reaction));
369-
}
366+
if ((flags & BLOCK_EFFECT) !== 0 && eager_block_effects !== null) {
367+
eager_block_effects.add(/** @type {Effect} */ (reaction));
370368
}
371369

372370
schedule_effect(/** @type {Effect} */ (reaction));

packages/svelte/src/internal/client/runtime.js

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -21,7 +21,8 @@ import {
2121
REACTION_IS_UPDATING,
2222
STALE_REACTION,
2323
ERROR_VALUE,
24-
WAS_MARKED
24+
WAS_MARKED,
25+
MANAGED_EFFECT
2526
} from './constants.js';
2627
import { old_values } from './reactivity/sources.js';
2728
import {
@@ -421,7 +422,7 @@ export function update_effect(effect) {
421422
}
422423

423424
try {
424-
if ((flags & BLOCK_EFFECT) !== 0) {
425+
if ((flags & (BLOCK_EFFECT | MANAGED_EFFECT)) !== 0) {
425426
destroy_block_effect_children(effect);
426427
} else {
427428
destroy_effect_children(effect);
Lines changed: 60 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,60 @@
1+
import { tick } from 'svelte';
2+
import { test } from '../../test';
3+
4+
export default test({
5+
async test({ assert, target }) {
6+
const [fork, commit] = target.querySelectorAll('button');
7+
8+
fork.click();
9+
await tick();
10+
assert.htmlEqual(
11+
target.innerHTML,
12+
`
13+
<button>fork</button>
14+
<button>commit</button>
15+
<p style="">foo</p>
16+
<p style="">foo</p>
17+
<p>foo</p>
18+
`
19+
);
20+
21+
commit.click();
22+
await tick();
23+
assert.htmlEqual(
24+
target.innerHTML,
25+
`
26+
<button>fork</button>
27+
<button>commit</button>
28+
<p style="color: red;">foo</p>
29+
<p style="color: red;" data-attached=true>foo</p>
30+
<p data-attached=true>foo</p>
31+
`
32+
);
33+
34+
fork.click();
35+
await tick();
36+
assert.htmlEqual(
37+
target.innerHTML,
38+
`
39+
<button>fork</button>
40+
<button>commit</button>
41+
<p style="color: red;">foo</p>
42+
<p style="color: red;" data-attached=true>foo</p>
43+
<p data-attached=true>foo</p>
44+
`
45+
);
46+
47+
commit.click();
48+
await tick();
49+
assert.htmlEqual(
50+
target.innerHTML,
51+
`
52+
<button>fork</button>
53+
<button>commit</button>
54+
<p style="">foo</p>
55+
<p style="">foo</p>
56+
<p>foo</p>
57+
`
58+
);
59+
}
60+
});
Lines changed: 28 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,28 @@
1+
<script>
2+
import { fork } from "svelte";
3+
import { createAttachmentKey } from "svelte/attachments";
4+
5+
let style = $state('');
6+
let attach = $state(undefined);
7+
8+
let forked;
9+
</script>
10+
11+
<button onclick={()=>{
12+
forked = fork(()=>{
13+
style = style ? '' : 'color: red';
14+
attach = attach ? undefined : (node) => {
15+
node.setAttribute('data-attached', 'true');
16+
return () => node.removeAttribute('data-attached');
17+
};
18+
})
19+
}}>fork</button>
20+
21+
<button onclick={()=>{
22+
forked.commit();
23+
}}>commit</button>
24+
25+
<!-- force $.attribute_effect, which uses a block effect -->
26+
<p {...{style}}>foo</p>
27+
<p {...{style, [createAttachmentKey()]: attach}}>foo</p>
28+
<p {@attach attach}>foo</p>

0 commit comments

Comments
 (0)