Skip to content

Commit 8175588

Browse files
authored
chore: refactor internal signal dependency heuristic (#12881)
* chore: remove redundant signal logic * more tweaks * more tweaks * refactor * tweak
1 parent 4512462 commit 8175588

File tree

7 files changed

+42
-60
lines changed

7 files changed

+42
-60
lines changed

.changeset/hot-kangaroos-invite.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+
chore: refactor internal signal dependency heuristic

packages/svelte/src/internal/client/dom/template.js

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -39,7 +39,7 @@ export function template(content, flags) {
3939
return hydrate_node;
4040
}
4141

42-
if (!node) {
42+
if (node === undefined) {
4343
node = create_fragment_from_html(has_start ? content : '<!>' + content);
4444
if (!is_fragment) node = /** @type {Node} */ (get_first_child(node));
4545
}

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

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -375,8 +375,10 @@ export function destroy_effect(effect, remove_dom = true) {
375375
remove_reactions(effect, 0);
376376
set_signal_status(effect, DESTROYED);
377377

378-
if (effect.transitions) {
379-
for (const transition of effect.transitions) {
378+
var transitions = effect.transitions;
379+
380+
if (transitions !== null) {
381+
for (const transition of transitions) {
380382
transition.stop();
381383
}
382384
}

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

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -139,8 +139,10 @@ function mark_reactions(signal, status) {
139139
if (reactions === null) return;
140140

141141
var runes = is_runes();
142+
var length = reactions.length;
142143

143-
for (var reaction of reactions) {
144+
for (var i = 0; i < length; i++) {
145+
var reaction = reactions[i];
144146
var flags = reaction.f;
145147

146148
// Skip any effects that are already dirty

packages/svelte/src/internal/client/reactivity/types.d.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -9,7 +9,7 @@ export interface Signal {
99

1010
export interface Value<V = unknown> extends Signal {
1111
/** Signals that read from this signal */
12-
reactions: null | Set<Reaction>;
12+
reactions: null | Reaction[];
1313
/** Equality function */
1414
equals: Equals;
1515
/** The latest value for this signal */

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

Lines changed: 20 additions & 47 deletions
Original file line numberDiff line numberDiff line change
@@ -169,7 +169,7 @@ export function check_dirtiness(reaction) {
169169

170170
if ((flags & DISCONNECTED) !== 0) {
171171
for (i = 0; i < dependencies.length; i++) {
172-
(dependencies[i].reactions ??= new Set()).add(reaction);
172+
(dependencies[i].reactions ??= []).push(reaction);
173173
}
174174

175175
reaction.f ^= DISCONNECTED;
@@ -188,11 +188,11 @@ export function check_dirtiness(reaction) {
188188

189189
if (is_unowned) {
190190
// TODO is there a more logical place to do this work?
191-
if (!current_skip_reaction && !dependency?.reactions?.has(reaction)) {
191+
if (!current_skip_reaction && !dependency?.reactions?.includes(reaction)) {
192192
// If we are working with an unowned signal as part of an effect (due to !current_skip_reaction)
193193
// and the version hasn't changed, we still need to check that this reaction
194194
// if linked to the dependency source – otherwise future updates will not be caught.
195-
(dependency.reactions ??= new Set()).add(reaction);
195+
(dependency.reactions ??= []).push(reaction);
196196
}
197197
}
198198
}
@@ -291,26 +291,9 @@ export function update_reaction(reaction) {
291291
var deps = reaction.deps;
292292

293293
if (new_deps !== null) {
294-
var dependency;
295294
var i;
296295

297-
if (deps !== null) {
298-
/** All dependencies of the reaction, including those tracked on the previous run */
299-
var array = skipped_deps === 0 ? new_deps : deps.slice(0, skipped_deps).concat(new_deps);
300-
301-
// If we have more than 16 elements in the array then use a Set for faster performance
302-
// TODO: evaluate if we should always just use a Set or not here?
303-
var set = array.length > 16 ? new Set(array) : null;
304-
305-
// Remove dependencies that should no longer be tracked
306-
for (i = skipped_deps; i < deps.length; i++) {
307-
dependency = deps[i];
308-
309-
if (set !== null ? !set.has(dependency) : !array.includes(dependency)) {
310-
remove_reaction(reaction, dependency);
311-
}
312-
}
313-
}
296+
remove_reactions(reaction, skipped_deps);
314297

315298
if (deps !== null && skipped_deps > 0) {
316299
deps.length = skipped_deps + new_deps.length;
@@ -323,7 +306,7 @@ export function update_reaction(reaction) {
323306

324307
if (!current_skip_reaction) {
325308
for (i = skipped_deps; i < deps.length; i++) {
326-
(deps[i].reactions ??= new Set()).add(reaction);
309+
(deps[i].reactions ??= []).push(reaction);
327310
}
328311
}
329312
} else if (deps !== null && skipped_deps < deps.length) {
@@ -350,9 +333,16 @@ export function update_reaction(reaction) {
350333
function remove_reaction(signal, dependency) {
351334
let reactions = dependency.reactions;
352335
if (reactions !== null) {
353-
reactions.delete(signal);
354-
if (reactions.size === 0) {
355-
reactions = dependency.reactions = null;
336+
var index = reactions.indexOf(signal);
337+
if (index !== -1) {
338+
var new_length = reactions.length - 1;
339+
if (new_length === 0) {
340+
reactions = dependency.reactions = null;
341+
} else {
342+
// Swap with last element and then remove.
343+
reactions[index] = reactions[new_length];
344+
reactions.pop();
345+
}
356346
}
357347
}
358348
// If the derived has no reactions, then we can disconnect it from the graph,
@@ -377,19 +367,8 @@ export function remove_reactions(signal, start_index) {
377367
var dependencies = signal.deps;
378368
if (dependencies === null) return;
379369

380-
var active_dependencies = start_index === 0 ? null : dependencies.slice(0, start_index);
381-
var seen = new Set();
382-
383370
for (var i = start_index; i < dependencies.length; i++) {
384-
var dependency = dependencies[i];
385-
386-
if (seen.has(dependency)) continue;
387-
seen.add(dependency);
388-
389-
// Avoid removing a reaction if we know that it is active (start_index will not be 0)
390-
if (active_dependencies === null || !active_dependencies.includes(dependency)) {
391-
remove_reaction(signal, dependency);
392-
}
371+
remove_reaction(signal, dependencies[i]);
393372
}
394373
}
395374

@@ -726,16 +705,10 @@ export function get(signal) {
726705
// rather than updating `new_deps`, which creates GC cost
727706
if (new_deps === null && deps !== null && deps[skipped_deps] === signal) {
728707
skipped_deps++;
729-
}
730-
731-
// Otherwise, create or push to `new_deps`, but only if this
732-
// dependency wasn't the last one that was accessed
733-
else if (deps === null || skipped_deps === 0 || deps[skipped_deps - 1] !== signal) {
734-
if (new_deps === null) {
735-
new_deps = [signal];
736-
} else if (new_deps[new_deps.length - 1] !== signal) {
737-
new_deps.push(signal);
738-
}
708+
} else if (new_deps === null) {
709+
new_deps = [signal];
710+
} else {
711+
new_deps.push(signal);
739712
}
740713

741714
if (

packages/svelte/tests/signals/test.ts

Lines changed: 8 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -236,13 +236,13 @@ describe('signals', () => {
236236
return () => {
237237
flushSync(() => set(count, 1));
238238
// Ensure we're not leaking consumers
239-
assert.deepEqual(count.reactions?.size, 1);
239+
assert.deepEqual(count.reactions?.length, 1);
240240
flushSync(() => set(count, 2));
241241
// Ensure we're not leaking consumers
242-
assert.deepEqual(count.reactions?.size, 1);
242+
assert.deepEqual(count.reactions?.length, 1);
243243
flushSync(() => set(count, 3));
244244
// Ensure we're not leaking consumers
245-
assert.deepEqual(count.reactions?.size, 1);
245+
assert.deepEqual(count.reactions?.length, 1);
246246
assert.deepEqual(log, [0, 1, 2, 3]);
247247
};
248248
});
@@ -304,8 +304,8 @@ describe('signals', () => {
304304
flushSync(() => set(count, 4));
305305
flushSync(() => set(count, 0));
306306
// Ensure we're not leaking consumers
307-
assert.deepEqual(count.reactions?.size, 1);
308-
assert.deepEqual(calc.reactions?.size, 1);
307+
assert.deepEqual(count.reactions?.length, 2);
308+
assert.deepEqual(calc.reactions?.length, 1);
309309
assert.deepEqual(log, [0, 2, 'limit', 0]);
310310
destroy();
311311
// Ensure we're not leaking consumers
@@ -484,7 +484,7 @@ describe('signals', () => {
484484
return () => {
485485
flushSync();
486486
assert.equal(a?.deps?.length, 1);
487-
assert.equal(state?.reactions?.size, 1);
487+
assert.equal(state?.reactions?.length, 1);
488488
destroy();
489489
assert.equal(a?.deps?.length, 1);
490490
assert.equal(state?.reactions, null);
@@ -658,12 +658,12 @@ describe('signals', () => {
658658
});
659659

660660
assert.equal($.get(d), 0);
661-
assert.equal(count.reactions?.size, 1);
661+
assert.equal(count.reactions?.length, 1);
662662
assert.equal(d.deps?.length, 1);
663663

664664
set(count, 1);
665665
assert.equal($.get(d), 2);
666-
assert.equal(count.reactions?.size, 1);
666+
assert.equal(count.reactions?.length, 1);
667667
assert.equal(d.deps?.length, 1);
668668

669669
destroy();

0 commit comments

Comments
 (0)