Skip to content
Merged
Show file tree
Hide file tree
Changes from 1 commit
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 5 additions & 0 deletions .changeset/curvy-clouds-cut.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
'svelte': patch
---

fix: don't execute attachments and attribute effects eagerly
5 changes: 5 additions & 0 deletions packages/svelte/src/internal/client/constants.js
Original file line number Diff line number Diff line change
Expand Up @@ -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
/**
Expand Down
Original file line number Diff line number Diff line change
@@ -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
Expand Down Expand Up @@ -29,5 +30,5 @@ export function attach(node, get_fn) {
});
}
}
});
}, BLOCK_NON_EAGER);
}
Original file line number Diff line number Diff line change
Expand Up @@ -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 {
Expand Down Expand Up @@ -540,7 +540,7 @@ export function attribute_effect(
}

prev = current;
});
}, BLOCK_NON_EAGER);

if (is_select) {
var select = /** @type {HTMLSelectElement} */ (element);
Expand Down
9 changes: 5 additions & 4 deletions packages/svelte/src/internal/client/reactivity/batch.js
Original file line number Diff line number Diff line change
Expand Up @@ -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';
Expand Down Expand Up @@ -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);
Expand Down Expand Up @@ -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);
Expand Down Expand Up @@ -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;
}
Expand Down
13 changes: 8 additions & 5 deletions packages/svelte/src/internal/client/reactivity/sources.js
Original file line number Diff line number Diff line change
Expand Up @@ -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';
Expand Down Expand Up @@ -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));
Expand Down
Original file line number Diff line number Diff line change
@@ -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,
`
<button>fork</button>
<button>commit</button>
<p style="">foo</p>
<p style="">foo</p>
<p>foo</p>
`
);

commit.click();
await tick();
assert.htmlEqual(
target.innerHTML,
`
<button>fork</button>
<button>commit</button>
<p style="color: red;">foo</p>
<p style="color: red;" data-attached=true>foo</p>
<p data-attached=true>foo</p>
`
);

fork.click();
await tick();
assert.htmlEqual(
target.innerHTML,
`
<button>fork</button>
<button>commit</button>
<p style="color: red;">foo</p>
<p style="color: red;" data-attached=true>foo</p>
<p data-attached=true>foo</p>
`
);

commit.click();
await tick();
assert.htmlEqual(
target.innerHTML,
`
<button>fork</button>
<button>commit</button>
<p style="">foo</p>
<p style="">foo</p>
<p>foo</p>
`
);
}
});
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
<script>
import { fork } from "svelte";
import { createAttachmentKey } from "svelte/attachments";

let style = $state('');
let attach = $state(undefined);

let forked;
</script>

<button onclick={()=>{
forked = fork(()=>{
style = style ? '' : 'color: red';
attach = attach ? undefined : (node) => {
node.setAttribute('data-attached', 'true');
return () => node.removeAttribute('data-attached');
};
})
}}>fork</button>

<button onclick={()=>{
forked.commit();
}}>commit</button>

<!-- force $.attribute_effect, which uses a block effect -->
<p {...{style}}>foo</p>
<p {...{style, [createAttachmentKey()]: attach}}>foo</p>
<p {@attach attach}>foo</p>
Loading