fix: don't mark deriveds while an effect is updating (#18124)

Fixes #18123

This makes setting state inside effects slightly slower theoretically
(since they hit the new guard), but I verified that the original issue
for which we introduced this (#16658) is still fast with this change.

The more we add logic to this the more I think we should investigate
switching to a different mechanism. I tried using a `Set` previously but
it did hurt the benchmarks a bit - might try to revisit a variant of
this.

---------

Co-authored-by: github-actions[bot] <github-actions[bot]@users.noreply.github.com>
pull/18125/head
Simon H 3 months ago committed by GitHub
parent 51736e576d
commit 8e7319063a
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194

@ -0,0 +1,5 @@
---
'svelte': patch
---
fix: don't mark deriveds while an effect is updating

@ -48,7 +48,8 @@ export const EFFECT_OFFSCREEN = 1 << 25;
/**
* Tells that we marked this derived and its reactions as visited during the "mark as (maybe) dirty"-phase.
* Will be lifted during execution of the derived and during checking its dirty state (both are necessary
* because a derived might be checked but not executed).
* because a derived might be checked but not executed). This is a pure performance optimization flag and
* should not be used for any other purpose!
*/
export const WAS_MARKED = 1 << 16;

@ -27,7 +27,8 @@ import {
ROOT_EFFECT,
ASYNC,
WAS_MARKED,
CONNECTED
CONNECTED,
REACTION_IS_UPDATING
} from '#client/constants';
import * as e from '../errors.js';
import { legacy_mode_flag, tracing_mode_flag } from '../../flags/index.js';
@ -356,8 +357,11 @@ function mark_reactions(signal, status, updated_during_traversal) {
batch_values?.delete(derived);
if ((flags & WAS_MARKED) === 0) {
// Only connected deriveds can be reliably unmarked right away
if (flags & CONNECTED) {
// Only connected deriveds being executed outside the update cycle can be reliably unmarked right away
if (
flags & CONNECTED &&
(active_effect === null || (active_effect.f & REACTION_IS_UPDATING) === 0)
) {
reaction.f |= WAS_MARKED;
}

@ -0,0 +1,7 @@
<script>
import { store } from "./store.svelte.js";
// This write marks the derived in main.svelte before it has reactions added to it.
// This test checks that this does not cause the WAS_MARKED logic to incorrectly skip marking the derived subsequently.
store.set("child-init-write", Math.random());
</script>

@ -0,0 +1,29 @@
import { flushSync } from 'svelte';
import { test } from '../../test';
export default test({
async test({ assert, target }) {
const [show, hide] = target.querySelectorAll('button');
hide.click();
flushSync();
assert.htmlEqual(
target.innerHTML,
`
<button>show</button>
<button>hide</button>
`
);
show.click();
flushSync();
assert.htmlEqual(
target.innerHTML,
`
<button>show</button>
<button>hide</button>
<div>visible</div>
`
);
}
});

@ -0,0 +1,14 @@
<script>
import Child from "./Child.svelte";
import { store } from "./store.svelte.js";
const visible = $derived(store.get("visible"));
const visible2 = $derived(visible);
</script>
<button onclick={() => store.set("visible", true)}>show</button>
<button onclick={() => store.set("visible", false)}>hide</button>
{#if visible2}
<Child />
<div>visible</div>
{/if}

@ -0,0 +1,13 @@
class RawStore {
values = $state.raw({ visible: true });
get(key) {
return this.values[key];
}
set(key, value) {
this.values = { ...this.values, [key]: value };
}
}
export const store = new RawStore();
Loading…
Cancel
Save