Skip to content

Commit

Permalink
fix: ensure if block is executed in correct order (#10053)
Browse files Browse the repository at this point in the history
* fix: ensure if block is executed in correct order

* alternative approach

* improve algo

* optimize

* lint
  • Loading branch information
trueadm committed Jan 2, 2024
1 parent 98a72f5 commit 1e33ed5
Show file tree
Hide file tree
Showing 5 changed files with 120 additions and 36 deletions.
5 changes: 5 additions & 0 deletions .changeset/unlucky-trees-lick.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
'svelte': patch
---

fix: ensure if block is executed in correct order
104 changes: 68 additions & 36 deletions packages/svelte/src/internal/client/runtime.js
Original file line number Diff line number Diff line change
Expand Up @@ -349,7 +349,21 @@ function execute_signal_fn(signal) {

if (current_dependencies !== null) {
let i;
remove_consumer(signal, current_dependencies_index, false);
if (dependencies !== null) {
const dep_length = dependencies.length;
// If we have more than 16 elements in the array then use a Set for faster performance
// TODO: evaluate if we should always just use a Set or not here?
const current_dependencies_set = dep_length > 16 ? new Set(current_dependencies) : null;
for (i = current_dependencies_index; i < dep_length; i++) {
const dependency = dependencies[i];
if (
(current_dependencies_set !== null && !current_dependencies_set.has(dependency)) ||
!current_dependencies.includes(dependency)
) {
remove_consumer(signal, dependency, false);
}
}
}

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

if (dependency.c === null) {
if (consumers === null) {
dependency.c = [signal];
} else {
dependency.c.push(signal);
} else if (consumers[consumers.length - 1] !== signal) {
consumers.push(signal);
}
}
}
} else if (dependencies !== null && current_dependencies_index < dependencies.length) {
remove_consumer(signal, current_dependencies_index, false);
remove_consumers(signal, current_dependencies_index, false);
dependencies.length = current_dependencies_index;
}
return res;
Expand All @@ -390,43 +405,54 @@ function execute_signal_fn(signal) {
}
}

/**
* @template V
* @param {import('./types.js').ComputationSignal<V>} signal
* @param {import('./types.js').Signal<V>} dependency
* @param {boolean} remove_unowned
* @returns {void}
*/
function remove_consumer(signal, dependency, remove_unowned) {
const consumers = dependency.c;
let consumers_length = 0;
if (consumers !== null) {
consumers_length = consumers.length - 1;
const index = consumers.indexOf(signal);
if (index !== -1) {
if (consumers_length === 0) {
dependency.c = null;
} else {
// Swap with last element and then remove.
consumers[index] = consumers[consumers_length];
consumers.pop();
}
}
}
if (remove_unowned && consumers_length === 0 && (dependency.f & UNOWNED) !== 0) {
// If the signal is unowned then we need to make sure to change it to dirty.
set_signal_status(dependency, DIRTY);
remove_consumers(
/** @type {import('./types.js').ComputationSignal<V>} **/ (dependency),
0,
true
);
}
}

/**
* @template V
* @param {import('./types.js').ComputationSignal<V>} signal
* @param {number} start_index
* @param {boolean} remove_unowned
* @returns {void}
*/
function remove_consumer(signal, start_index, remove_unowned) {
function remove_consumers(signal, start_index, remove_unowned) {
const dependencies = signal.d;
if (dependencies !== null) {
let i;
for (i = start_index; i < dependencies.length; i++) {
const dependency = dependencies[i];
const consumers = dependency.c;
let consumers_length = 0;
if (consumers !== null) {
consumers_length = consumers.length - 1;
const index = consumers.indexOf(signal);
if (index !== -1) {
if (consumers_length === 0) {
dependency.c = null;
} else {
// Swap with last element and then remove.
consumers[index] = consumers[consumers_length];
consumers.pop();
}
}
}
if (remove_unowned && consumers_length === 0 && (dependency.f & UNOWNED) !== 0) {
// If the signal is unowned then we need to make sure to change it to dirty.
set_signal_status(dependency, DIRTY);
remove_consumer(
/** @type {import('./types.js').ComputationSignal<V>} **/ (dependency),
0,
true
);
}
remove_consumer(signal, dependency, remove_unowned);
}
}
}
Expand All @@ -446,7 +472,7 @@ function destroy_references(signal) {
if ((reference.f & IS_EFFECT) !== 0) {
destroy_signal(reference);
} else {
remove_consumer(reference, 0, true);
remove_consumers(reference, 0, true);
reference.d = null;
}
}
Expand Down Expand Up @@ -841,10 +867,16 @@ export function get(signal) {
!(unowned && current_effect !== null)
) {
current_dependencies_index++;
} else if (current_dependencies === null) {
current_dependencies = [signal];
} else if (signal !== current_dependencies[current_dependencies.length - 1]) {
current_dependencies.push(signal);
} else if (
dependencies === null ||
current_dependencies_index === 0 ||
dependencies[current_dependencies_index - 1] !== signal
) {
if (current_dependencies === null) {
current_dependencies = [signal];
} else if (signal !== current_dependencies[current_dependencies.length - 1]) {
current_dependencies.push(signal);
}
}
if (
current_untracked_writes !== null &&
Expand Down Expand Up @@ -1098,7 +1130,7 @@ export function destroy_signal(signal) {
const destroy = signal.y;
const flags = signal.f;
destroy_references(signal);
remove_consumer(signal, 0, true);
remove_consumers(signal, 0, true);
signal.i =
signal.r =
signal.y =
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
<script>
const { item } = $props();
</script>

<div>
{#if item}
{item.length}
{/if}
</div>
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
import { flushSync } from 'svelte';
import { test } from '../../test';

export default test({
async test({ assert, target, component }) {
const [b1, b2] = target.querySelectorAll('button');
assert.htmlEqual(
target.innerHTML,
'<div>5</div><div>5</div><div>3</div><button>set null</button><button>set object</button'
);
flushSync(() => {
b2.click();
});
assert.htmlEqual(
target.innerHTML,
'<div>5</div><div>5</div><div>3</div><button>set null</button><button>set object</button'
);
flushSync(() => {
b1.click();
});
assert.htmlEqual(
target.innerHTML,
'<div>5</div><div></div><div>3</div><button>set null</button><button>set object</button'
);
}
});
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
<script>
import Component from './Component.svelte';
let items = $state(['hello', 'world', 'bye']);
</script>

{#each items as item}
<Component {item} />
{/each}

<button onclick={() => (items[1] = null)}> set null </button>
<button onclick={() => (items[1] = 'hello')}> set object </button>

1 comment on commit 1e33ed5

@vercel
Copy link

@vercel vercel bot commented on 1e33ed5 Jan 2, 2024

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Successfully deployed to the following URLs:

svelte-5-preview – ./sites/svelte-5-preview

svelte-5-preview.vercel.app
svelte-5-preview-git-main-svelte.vercel.app
svelte-octane.vercel.app
svelte-5-preview-svelte.vercel.app

Please sign in to comment.