Skip to content

Commit b602c59

Browse files
trueadmdummdidumm
andauthored
fix: when re-connecting unowned deriveds, remove their unowned flag (#15255)
* fix: when an unowned derived is tracked again, remove unowned flag * fix: when an unowned derived is tracked again, remove unowned flag * test case * test case * cleanup unused logic * simplify * simplify * simplify * simplify * add test * fix other issue * back to original plan * Apply suggestions from code review --------- Co-authored-by: Simon H <[email protected]>
1 parent 02788f8 commit b602c59

File tree

7 files changed

+208
-73
lines changed

7 files changed

+208
-73
lines changed

.changeset/nasty-pigs-lay.md

+5
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
---
2+
'svelte': patch
3+
---
4+
5+
fix: when re-connecting unowned deriveds, remove their unowned flag

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

+1-10
Original file line numberDiff line numberDiff line change
@@ -1,18 +1,9 @@
11
/** @import { Derived, Effect } from '#client' */
22
import { DEV } from 'esm-env';
3-
import {
4-
CLEAN,
5-
DERIVED,
6-
DESTROYED,
7-
DIRTY,
8-
EFFECT_HAS_DERIVED,
9-
MAYBE_DIRTY,
10-
UNOWNED
11-
} from '../constants.js';
3+
import { CLEAN, DERIVED, DIRTY, EFFECT_HAS_DERIVED, MAYBE_DIRTY, UNOWNED } from '../constants.js';
124
import {
135
active_reaction,
146
active_effect,
15-
remove_reactions,
167
set_signal_status,
178
skip_reaction,
189
update_reaction,

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

+23-37
Original file line numberDiff line numberDiff line change
@@ -10,7 +10,15 @@ import {
1010
import { get_descriptor, is_function } from '../../shared/utils.js';
1111
import { mutable_source, set, source, update } from './sources.js';
1212
import { derived, derived_safe_equal } from './deriveds.js';
13-
import { active_effect, get, captured_signals, set_active_effect, untrack } from '../runtime.js';
13+
import {
14+
active_effect,
15+
get,
16+
captured_signals,
17+
set_active_effect,
18+
untrack,
19+
active_reaction,
20+
set_active_reaction
21+
} from '../runtime.js';
1422
import { safe_equals } from './equality.js';
1523
import * as e from '../errors.js';
1624
import {
@@ -241,26 +249,6 @@ export function spread_props(...props) {
241249
return new Proxy({ props }, spread_props_handler);
242250
}
243251

244-
/**
245-
* @template T
246-
* @param {() => T} fn
247-
* @returns {T}
248-
*/
249-
function with_parent_branch(fn) {
250-
var effect = active_effect;
251-
var previous_effect = active_effect;
252-
253-
while (effect !== null && (effect.f & (BRANCH_EFFECT | ROOT_EFFECT)) === 0) {
254-
effect = effect.parent;
255-
}
256-
try {
257-
set_active_effect(effect);
258-
return fn();
259-
} finally {
260-
set_active_effect(previous_effect);
261-
}
262-
}
263-
264252
/**
265253
* This function is responsible for synchronizing a possibly bound prop with the inner component state.
266254
* It is used whenever the compiler sees that the component writes to the prop, or when it has a default prop_value.
@@ -335,8 +323,8 @@ export function prop(props, key, flags, fallback) {
335323
} else {
336324
// Svelte 4 did not trigger updates when a primitive value was updated to the same value.
337325
// Replicate that behavior through using a derived
338-
var derived_getter = with_parent_branch(() =>
339-
(immutable ? derived : derived_safe_equal)(() => /** @type {V} */ (props[key]))
326+
var derived_getter = (immutable ? derived : derived_safe_equal)(
327+
() => /** @type {V} */ (props[key])
340328
);
341329
derived_getter.f |= LEGACY_DERIVED_PROP;
342330
getter = () => {
@@ -380,21 +368,19 @@ export function prop(props, key, flags, fallback) {
380368
// The derived returns the current value. The underlying mutable
381369
// source is written to from various places to persist this value.
382370
var inner_current_value = mutable_source(prop_value);
383-
var current_value = with_parent_branch(() =>
384-
derived(() => {
385-
var parent_value = getter();
386-
var child_value = get(inner_current_value);
387-
388-
if (from_child) {
389-
from_child = false;
390-
was_from_child = true;
391-
return child_value;
392-
}
371+
var current_value = derived(() => {
372+
var parent_value = getter();
373+
var child_value = get(inner_current_value);
374+
375+
if (from_child) {
376+
from_child = false;
377+
was_from_child = true;
378+
return child_value;
379+
}
393380

394-
was_from_child = false;
395-
return (inner_current_value.v = parent_value);
396-
})
397-
);
381+
was_from_child = false;
382+
return (inner_current_value.v = parent_value);
383+
});
398384

399385
if (!immutable) current_value.equals = safe_equals;
400386

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

+12-3
Original file line numberDiff line numberDiff line change
@@ -184,19 +184,28 @@ export function check_dirtiness(reaction) {
184184
// If we are working with a disconnected or an unowned signal that is now connected (due to an active effect)
185185
// then we need to re-connect the reaction to the dependency
186186
if (is_disconnected || is_unowned_connected) {
187+
var derived = /** @type {Derived} */ (reaction);
188+
var parent = derived.parent;
189+
187190
for (i = 0; i < length; i++) {
188191
dependency = dependencies[i];
189192

190193
// We always re-add all reactions (even duplicates) if the derived was
191194
// previously disconnected, however we don't if it was unowned as we
192195
// de-duplicate dependencies in that case
193-
if (is_disconnected || !dependency?.reactions?.includes(reaction)) {
194-
(dependency.reactions ??= []).push(reaction);
196+
if (is_disconnected || !dependency?.reactions?.includes(derived)) {
197+
(dependency.reactions ??= []).push(derived);
195198
}
196199
}
197200

198201
if (is_disconnected) {
199-
reaction.f ^= DISCONNECTED;
202+
derived.f ^= DISCONNECTED;
203+
}
204+
// If the unowned derived is now fully connected to the graph again (it's unowned and reconnected, has a parent
205+
// and the parent is not unowned), then we can mark it as connected again, removing the need for the unowned
206+
// flag
207+
if (is_unowned_connected && parent !== null && (parent.f & UNOWNED) === 0) {
208+
derived.f ^= UNOWNED;
200209
}
201210
}
202211

Original file line numberDiff line numberDiff line change
@@ -0,0 +1,25 @@
1+
import { flushSync } from 'svelte';
2+
import { test } from '../../test';
3+
4+
export default test({
5+
async test({ assert, target }) {
6+
let [btn1, btn2] = target.querySelectorAll('button');
7+
8+
btn1?.click();
9+
flushSync();
10+
11+
btn2?.click();
12+
flushSync();
13+
14+
btn1?.click();
15+
flushSync();
16+
17+
btn1?.click();
18+
flushSync();
19+
20+
assert.htmlEqual(
21+
target.innerHTML,
22+
`<button>linked.current</button>\n3\n<button>count</button>\n1`
23+
);
24+
}
25+
});
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,18 @@
1+
<script>
2+
import { untrack } from 'svelte';
3+
4+
let count = $state(0);
5+
let state = $state({current: count});
6+
7+
let linked = $derived.by(() => {
8+
count;
9+
10+
untrack(() => state.current = count);
11+
return untrack(() => state);
12+
});
13+
14+
linked.current++;
15+
</script>
16+
17+
<button onclick={() => linked.current++}>linked.current</button> {linked.current}
18+
<button onclick={() => count++}>count</button> {count}

packages/svelte/tests/signals/test.ts

+124-23
Original file line numberDiff line numberDiff line change
@@ -224,20 +224,75 @@ describe('signals', () => {
224224
};
225225
});
226226

227-
test('effects correctly handle unowned derived values that do not change', () => {
228-
const log: number[] = [];
227+
test('https://perf.js.hyoo.ru/#!bench=9h2as6_u0mfnn #2', () => {
228+
let res: number[] = [];
229229

230-
let count = state(0);
231-
const read = () => {
232-
const x = derived(() => ({ count: $.get(count) }));
233-
return $.get(x);
230+
const numbers = Array.from({ length: 2 }, (_, i) => i);
231+
const fib = (n: number): number => (n < 2 ? 1 : fib(n - 1) + fib(n - 2));
232+
const hard = (n: number, l: string) => n + fib(16);
233+
234+
const A = state(0);
235+
const B = state(0);
236+
237+
return () => {
238+
const C = derived(() => ($.get(A) % 2) + ($.get(B) % 2));
239+
const D = derived(() => numbers.map((i) => i + ($.get(A) % 2) - ($.get(B) % 2)));
240+
const E = derived(() => hard($.get(C) + $.get(A) + $.get(D)[0]!, 'E'));
241+
const F = derived(() => hard($.get(D)[0]! && $.get(B), 'F'));
242+
const G = derived(() => $.get(C) + ($.get(C) || $.get(E) % 2) + $.get(D)[0]! + $.get(F));
243+
244+
const destroy = effect_root(() => {
245+
effect(() => {
246+
res.push(hard($.get(G), 'H'));
247+
});
248+
effect(() => {
249+
res.push($.get(G));
250+
});
251+
effect(() => {
252+
res.push(hard($.get(F), 'J'));
253+
});
254+
});
255+
256+
flushSync();
257+
258+
let i = 2;
259+
while (--i) {
260+
res.length = 0;
261+
set(B, 1);
262+
set(A, 1 + i * 2);
263+
flushSync();
264+
265+
set(A, 2 + i * 2);
266+
set(B, 2);
267+
flushSync();
268+
269+
assert.equal(res.length, 4);
270+
assert.deepEqual(res, [3198, 1601, 3195, 1598]);
271+
}
272+
273+
destroy();
274+
assert(A.reactions === null);
275+
assert(B.reactions === null);
234276
};
235-
const derivedCount = derived(() => read().count);
236-
user_effect(() => {
237-
log.push($.get(derivedCount));
238-
});
277+
});
278+
279+
test('effects correctly handle unowned derived values that do not change', () => {
280+
const log: number[] = [];
239281

240282
return () => {
283+
let count = state(0);
284+
const read = () => {
285+
const x = derived(() => ({ count: $.get(count) }));
286+
return $.get(x);
287+
};
288+
const derivedCount = derived(() => read().count);
289+
290+
const destroy = effect_root(() => {
291+
user_effect(() => {
292+
log.push($.get(derivedCount));
293+
});
294+
});
295+
241296
flushSync(() => set(count, 1));
242297
// Ensure we're not leaking consumers
243298
assert.deepEqual(count.reactions?.length, 1);
@@ -248,6 +303,8 @@ describe('signals', () => {
248303
// Ensure we're not leaking consumers
249304
assert.deepEqual(count.reactions?.length, 1);
250305
assert.deepEqual(log, [0, 1, 2, 3]);
306+
307+
destroy();
251308
};
252309
});
253310

@@ -343,25 +400,69 @@ describe('signals', () => {
343400
};
344401
});
345402

346-
let some_state = state({});
347-
let some_deps = derived(() => {
348-
return [$.get(some_state)];
349-
});
350-
351403
test('two effects with an unowned derived that has some dependencies', () => {
352404
const log: Array<Array<any>> = [];
353405

354-
render_effect(() => {
355-
log.push($.get(some_deps));
356-
});
406+
return () => {
407+
let some_state = state({});
408+
let some_deps = derived(() => {
409+
return [$.get(some_state)];
410+
});
411+
let destroy2: any;
357412

358-
render_effect(() => {
359-
log.push($.get(some_deps));
360-
});
413+
const destroy = effect_root(() => {
414+
render_effect(() => {
415+
$.untrack(() => {
416+
log.push($.get(some_deps));
417+
});
418+
});
361419

362-
return () => {
420+
destroy2 = effect_root(() => {
421+
render_effect(() => {
422+
log.push($.get(some_deps));
423+
});
424+
425+
render_effect(() => {
426+
log.push($.get(some_deps));
427+
});
428+
});
429+
});
430+
431+
set(some_state, {});
432+
flushSync();
433+
434+
assert.deepEqual(log, [[{}], [{}], [{}], [{}], [{}]]);
435+
436+
destroy2();
437+
438+
set(some_state, {});
363439
flushSync();
364-
assert.deepEqual(log, [[{}], [{}]]);
440+
441+
assert.deepEqual(log, [[{}], [{}], [{}], [{}], [{}]]);
442+
443+
log.length = 0;
444+
445+
const destroy3 = effect_root(() => {
446+
render_effect(() => {
447+
$.untrack(() => {
448+
log.push($.get(some_deps));
449+
});
450+
log.push($.get(some_deps));
451+
});
452+
});
453+
454+
set(some_state, {});
455+
flushSync();
456+
457+
assert.deepEqual(log, [[{}], [{}], [{}], [{}]]);
458+
459+
destroy3();
460+
461+
assert(some_state.reactions === null);
462+
463+
destroy();
464+
465+
assert(some_state.reactions === null);
365466
};
366467
});
367468

0 commit comments

Comments
 (0)