Skip to content

Commit 1e33ed5

Browse files
authored
fix: ensure if block is executed in correct order (#10053)
* fix: ensure if block is executed in correct order * alternative approach * improve algo * optimize * lint
1 parent 98a72f5 commit 1e33ed5

File tree

5 files changed

+120
-36
lines changed

5 files changed

+120
-36
lines changed

.changeset/unlucky-trees-lick.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: ensure if block is executed in correct order

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

Lines changed: 68 additions & 36 deletions
Original file line numberDiff line numberDiff line change
@@ -349,7 +349,21 @@ function execute_signal_fn(signal) {
349349

350350
if (current_dependencies !== null) {
351351
let i;
352-
remove_consumer(signal, current_dependencies_index, false);
352+
if (dependencies !== null) {
353+
const dep_length = dependencies.length;
354+
// If we have more than 16 elements in the array then use a Set for faster performance
355+
// TODO: evaluate if we should always just use a Set or not here?
356+
const current_dependencies_set = dep_length > 16 ? new Set(current_dependencies) : null;
357+
for (i = current_dependencies_index; i < dep_length; i++) {
358+
const dependency = dependencies[i];
359+
if (
360+
(current_dependencies_set !== null && !current_dependencies_set.has(dependency)) ||
361+
!current_dependencies.includes(dependency)
362+
) {
363+
remove_consumer(signal, dependency, false);
364+
}
365+
}
366+
}
353367

354368
if (dependencies !== null && current_dependencies_index > 0) {
355369
dependencies.length = current_dependencies_index + current_dependencies.length;
@@ -365,16 +379,17 @@ function execute_signal_fn(signal) {
365379
if (!current_skip_consumer) {
366380
for (i = current_dependencies_index; i < dependencies.length; i++) {
367381
const dependency = dependencies[i];
382+
const consumers = dependency.c;
368383

369-
if (dependency.c === null) {
384+
if (consumers === null) {
370385
dependency.c = [signal];
371-
} else {
372-
dependency.c.push(signal);
386+
} else if (consumers[consumers.length - 1] !== signal) {
387+
consumers.push(signal);
373388
}
374389
}
375390
}
376391
} else if (dependencies !== null && current_dependencies_index < dependencies.length) {
377-
remove_consumer(signal, current_dependencies_index, false);
392+
remove_consumers(signal, current_dependencies_index, false);
378393
dependencies.length = current_dependencies_index;
379394
}
380395
return res;
@@ -390,43 +405,54 @@ function execute_signal_fn(signal) {
390405
}
391406
}
392407

408+
/**
409+
* @template V
410+
* @param {import('./types.js').ComputationSignal<V>} signal
411+
* @param {import('./types.js').Signal<V>} dependency
412+
* @param {boolean} remove_unowned
413+
* @returns {void}
414+
*/
415+
function remove_consumer(signal, dependency, remove_unowned) {
416+
const consumers = dependency.c;
417+
let consumers_length = 0;
418+
if (consumers !== null) {
419+
consumers_length = consumers.length - 1;
420+
const index = consumers.indexOf(signal);
421+
if (index !== -1) {
422+
if (consumers_length === 0) {
423+
dependency.c = null;
424+
} else {
425+
// Swap with last element and then remove.
426+
consumers[index] = consumers[consumers_length];
427+
consumers.pop();
428+
}
429+
}
430+
}
431+
if (remove_unowned && consumers_length === 0 && (dependency.f & UNOWNED) !== 0) {
432+
// If the signal is unowned then we need to make sure to change it to dirty.
433+
set_signal_status(dependency, DIRTY);
434+
remove_consumers(
435+
/** @type {import('./types.js').ComputationSignal<V>} **/ (dependency),
436+
0,
437+
true
438+
);
439+
}
440+
}
441+
393442
/**
394443
* @template V
395444
* @param {import('./types.js').ComputationSignal<V>} signal
396445
* @param {number} start_index
397446
* @param {boolean} remove_unowned
398447
* @returns {void}
399448
*/
400-
function remove_consumer(signal, start_index, remove_unowned) {
449+
function remove_consumers(signal, start_index, remove_unowned) {
401450
const dependencies = signal.d;
402451
if (dependencies !== null) {
403452
let i;
404453
for (i = start_index; i < dependencies.length; i++) {
405454
const dependency = dependencies[i];
406-
const consumers = dependency.c;
407-
let consumers_length = 0;
408-
if (consumers !== null) {
409-
consumers_length = consumers.length - 1;
410-
const index = consumers.indexOf(signal);
411-
if (index !== -1) {
412-
if (consumers_length === 0) {
413-
dependency.c = null;
414-
} else {
415-
// Swap with last element and then remove.
416-
consumers[index] = consumers[consumers_length];
417-
consumers.pop();
418-
}
419-
}
420-
}
421-
if (remove_unowned && consumers_length === 0 && (dependency.f & UNOWNED) !== 0) {
422-
// If the signal is unowned then we need to make sure to change it to dirty.
423-
set_signal_status(dependency, DIRTY);
424-
remove_consumer(
425-
/** @type {import('./types.js').ComputationSignal<V>} **/ (dependency),
426-
0,
427-
true
428-
);
429-
}
455+
remove_consumer(signal, dependency, remove_unowned);
430456
}
431457
}
432458
}
@@ -446,7 +472,7 @@ function destroy_references(signal) {
446472
if ((reference.f & IS_EFFECT) !== 0) {
447473
destroy_signal(reference);
448474
} else {
449-
remove_consumer(reference, 0, true);
475+
remove_consumers(reference, 0, true);
450476
reference.d = null;
451477
}
452478
}
@@ -841,10 +867,16 @@ export function get(signal) {
841867
!(unowned && current_effect !== null)
842868
) {
843869
current_dependencies_index++;
844-
} else if (current_dependencies === null) {
845-
current_dependencies = [signal];
846-
} else if (signal !== current_dependencies[current_dependencies.length - 1]) {
847-
current_dependencies.push(signal);
870+
} else if (
871+
dependencies === null ||
872+
current_dependencies_index === 0 ||
873+
dependencies[current_dependencies_index - 1] !== signal
874+
) {
875+
if (current_dependencies === null) {
876+
current_dependencies = [signal];
877+
} else if (signal !== current_dependencies[current_dependencies.length - 1]) {
878+
current_dependencies.push(signal);
879+
}
848880
}
849881
if (
850882
current_untracked_writes !== null &&
@@ -1098,7 +1130,7 @@ export function destroy_signal(signal) {
10981130
const destroy = signal.y;
10991131
const flags = signal.f;
11001132
destroy_references(signal);
1101-
remove_consumer(signal, 0, true);
1133+
remove_consumers(signal, 0, true);
11021134
signal.i =
11031135
signal.r =
11041136
signal.y =
Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,9 @@
1+
<script>
2+
const { item } = $props();
3+
</script>
4+
5+
<div>
6+
{#if item}
7+
{item.length}
8+
{/if}
9+
</div>
Lines changed: 26 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,26 @@
1+
import { flushSync } from 'svelte';
2+
import { test } from '../../test';
3+
4+
export default test({
5+
async test({ assert, target, component }) {
6+
const [b1, b2] = target.querySelectorAll('button');
7+
assert.htmlEqual(
8+
target.innerHTML,
9+
'<div>5</div><div>5</div><div>3</div><button>set null</button><button>set object</button'
10+
);
11+
flushSync(() => {
12+
b2.click();
13+
});
14+
assert.htmlEqual(
15+
target.innerHTML,
16+
'<div>5</div><div>5</div><div>3</div><button>set null</button><button>set object</button'
17+
);
18+
flushSync(() => {
19+
b1.click();
20+
});
21+
assert.htmlEqual(
22+
target.innerHTML,
23+
'<div>5</div><div></div><div>3</div><button>set null</button><button>set object</button'
24+
);
25+
}
26+
});
Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,12 @@
1+
<script>
2+
import Component from './Component.svelte';
3+
4+
let items = $state(['hello', 'world', 'bye']);
5+
</script>
6+
7+
{#each items as item}
8+
<Component {item} />
9+
{/each}
10+
11+
<button onclick={() => (items[1] = null)}> set null </button>
12+
<button onclick={() => (items[1] = 'hello')}> set object </button>

0 commit comments

Comments
 (0)