fix: further improvements to effect scheduling and flushing (#10971)

* fix: improve effect scheduling

* fix: further improvements to effect scheduling and flushin

* add test

* simplify

* simplify

* lint

* fix e2e tests

* fix e2e tests

* simplify

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

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

Co-authored-by: Rich Harris <rich.harris@vercel.com>

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

Co-authored-by: Rich Harris <rich.harris@vercel.com>

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

Co-authored-by: Rich Harris <rich.harris@vercel.com>

* style tweak

---------

Co-authored-by: Rich Harris <rich.harris@vercel.com>
pull/10977/head
Dominic Gannaway 6 months ago committed by GitHub
parent 293f905a53
commit 8971910940
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194

@ -0,0 +1,5 @@
---
"svelte": patch
---
fix: further improvements to effect scheduling and flushing

@ -68,7 +68,11 @@ export function set_xlink_attribute(dom, attribute, value) {
*/ */
export function set_custom_element_data(node, prop, value) { export function set_custom_element_data(node, prop, value) {
if (prop in node) { if (prop in node) {
node[prop] = typeof node[prop] === 'boolean' && value === '' ? true : value; var curr_val = node[prop];
var next_val = typeof curr_val === 'boolean' && value === '' ? true : value;
if (typeof curr_val !== 'object' || curr_val !== next_val) {
node[prop] = next_val;
}
} else { } else {
set_attribute(node, prop, value); set_attribute(node, prop, value);
} }

@ -1,3 +1,4 @@
import { CLEAN } from '../../constants.js';
import { run, run_all } from '../../../shared/utils.js'; import { run, run_all } from '../../../shared/utils.js';
import { user_pre_effect, user_effect } from '../../reactivity/effects.js'; import { user_pre_effect, user_effect } from '../../reactivity/effects.js';
import { import {
@ -27,7 +28,7 @@ export function init() {
// are batched up with the current run. Avoids for example child components rerunning when they're // are batched up with the current run. Avoids for example child components rerunning when they're
// now hidden because beforeUpdate did set an if block to false. // now hidden because beforeUpdate did set an if block to false.
const parent = current_effect?.parent; const parent = current_effect?.parent;
if (parent != null) { if (parent != null && (parent.f & CLEAN) === 0) {
flush_local_render_effects(parent); flush_local_render_effects(parent);
} }
}); });

@ -424,8 +424,7 @@ function infinite_loop_guard() {
function flush_queued_root_effects(root_effects) { function flush_queued_root_effects(root_effects) {
for (var i = 0; i < root_effects.length; i++) { for (var i = 0; i < root_effects.length; i++) {
var signal = root_effects[i]; var signal = root_effects[i];
var effects = get_nested_effects(signal, RENDER_EFFECT | EFFECT); flush_nested_effects(signal, RENDER_EFFECT | EFFECT);
flush_queued_effects(effects);
} }
} }
@ -438,24 +437,13 @@ function flush_queued_effects(effects) {
if (length === 0) return; if (length === 0) return;
infinite_loop_guard(); infinite_loop_guard();
var previously_flushing_effect = is_flushing_effect; for (var i = 0; i < length; i++) {
is_flushing_effect = true; var effect = effects[i];
try {
for (var i = 0; i < length; i++) {
var signal = effects[i];
if ((signal.f & (DESTROYED | INERT)) === 0) { if ((effect.f & (DESTROYED | INERT)) === 0 && check_dirtiness(effect)) {
if (check_dirtiness(signal)) { execute_effect(effect);
execute_effect(signal);
}
}
} }
} finally {
is_flushing_effect = previously_flushing_effect;
} }
effects.length = 0;
} }
function process_microtask() { function process_microtask() {
@ -512,69 +500,49 @@ export function schedule_effect(signal) {
* @param {import('./types.js').Effect} effect * @param {import('./types.js').Effect} effect
* @param {number} filter_flags * @param {number} filter_flags
* @param {boolean} shallow * @param {boolean} shallow
* @param {import('./types.js').Effect[]} collected_render
* @param {import('./types.js').Effect[]} collected_user * @param {import('./types.js').Effect[]} collected_user
* @returns {void} * @returns {void}
*/ */
function recursively_collect_effects( function recursively_process_effects(effect, filter_flags, shallow, collected_user) {
effect,
filter_flags,
shallow,
collected_render,
collected_user
) {
var effects = effect.effects; var effects = effect.effects;
if (effects === null) return; if (effects === null) return;
var render = [];
var user = []; var user = [];
for (var i = 0; i < effects.length; i++) { for (var i = 0; i < effects.length; i++) {
var child = effects[i]; var child = effects[i];
var flags = child.f; var flags = child.f;
var is_inactive = (flags & (DESTROYED | INERT)) !== 0;
if (is_inactive) continue;
var is_branch = flags & BRANCH_EFFECT; var is_branch = flags & BRANCH_EFFECT;
var is_clean = (flags & CLEAN) !== 0;
if (is_branch) { if (is_branch) {
// Skip this branch if it's clean // Skip this branch if it's clean
if ((flags & CLEAN) !== 0) continue; if (is_clean) continue;
set_signal_status(child, CLEAN); set_signal_status(child, CLEAN);
} }
if ((flags & RENDER_EFFECT) !== 0) { if ((flags & RENDER_EFFECT) !== 0) {
if (is_branch) { if (is_branch) {
if (shallow) continue; if (shallow) continue;
recursively_collect_effects(child, filter_flags, false, collected_render, collected_user); recursively_process_effects(child, filter_flags, false, collected_user);
} else { } else {
render.push(child); if (check_dirtiness(child)) {
execute_effect(child);
}
recursively_process_effects(child, filter_flags, false, collected_user);
} }
} else if ((flags & EFFECT) !== 0) { } else if ((flags & EFFECT) !== 0) {
if (is_branch) { if (is_branch || is_clean) {
if (shallow) continue; if (shallow) continue;
recursively_collect_effects(child, filter_flags, false, collected_render, collected_user); recursively_process_effects(child, filter_flags, false, collected_user);
} else { } else {
user.push(child); user.push(child);
} }
} }
} }
if (render.length > 0) {
if ((filter_flags & RENDER_EFFECT) !== 0) {
collected_render.push(...render);
}
if (!shallow) {
for (i = 0; i < render.length; i++) {
recursively_collect_effects(
render[i],
filter_flags,
false,
collected_render,
collected_user
);
}
}
}
if (user.length > 0) { if (user.length > 0) {
if ((filter_flags & EFFECT) !== 0) { if ((filter_flags & EFFECT) !== 0) {
collected_user.push(...user); collected_user.push(...user);
@ -582,7 +550,7 @@ function recursively_collect_effects(
if (!shallow) { if (!shallow) {
for (i = 0; i < user.length; i++) { for (i = 0; i < user.length; i++) {
recursively_collect_effects(user[i], filter_flags, false, collected_render, collected_user); recursively_process_effects(user[i], filter_flags, false, collected_user);
} }
} }
} }
@ -597,23 +565,26 @@ function recursively_collect_effects(
* @param {import('./types.js').Effect} effect * @param {import('./types.js').Effect} effect
* @param {number} filter_flags * @param {number} filter_flags
* @param {boolean} [shallow] * @param {boolean} [shallow]
* @returns {import('./types.js').Effect[]} * @returns {void}
*/ */
function get_nested_effects(effect, filter_flags, shallow = false) { function flush_nested_effects(effect, filter_flags, shallow = false) {
/** /** @type {import('#client').Effect[]} */
* @type {import("./types.js").Effect[]}
*/
var render_effects = [];
/**
* @type {import("./types.js").Effect[]}
*/
var user_effects = []; var user_effects = [];
// When working with custom-elements, the root effects might not have a root
if (effect.effects === null && (effect.f & BRANCH_EFFECT) === 0) { var previously_flushing_effect = is_flushing_effect;
return [effect]; is_flushing_effect = true;
try {
// When working with custom elements, the root effects might not have a root
if (effect.effects === null && (effect.f & BRANCH_EFFECT) === 0) {
flush_queued_effects([effect]);
} else {
recursively_process_effects(effect, filter_flags, shallow, user_effects);
flush_queued_effects(user_effects);
}
} finally {
is_flushing_effect = previously_flushing_effect;
} }
recursively_collect_effects(effect, filter_flags, shallow, render_effects, user_effects);
return [...render_effects, ...user_effects];
} }
/** /**
@ -621,11 +592,9 @@ function get_nested_effects(effect, filter_flags, shallow = false) {
* @returns {void} * @returns {void}
*/ */
export function flush_local_render_effects(effect) { export function flush_local_render_effects(effect) {
/** // We are entering a new flush sequence, so ensure counter is reset.
* @type {import("./types.js").Effect[]} flush_count = 0;
*/ flush_nested_effects(effect, RENDER_EFFECT, true);
var render_effects = get_nested_effects(effect, RENDER_EFFECT, true);
flush_queued_effects(render_effects);
} }
/** /**

@ -0,0 +1,24 @@
<script>
import {beforeUpdate} from 'svelte';
let count1 = 0;
let count2 = 0;
function increaseCount1() {
count1++;
}
$: if (count2 < 10) {
increaseCount1();
}
$: if (count1 < 10) {
count2++;
}
beforeUpdate(() => {
// We don't need to do anything
});
</script>
<button on:click={() => count1++}>{count1} / {count2}</button>

@ -0,0 +1,11 @@
import { flushSync } from 'svelte';
import { test } from '../../test';
export default test({
async test({ assert, component, target }) {
const [btn1] = target.querySelectorAll('button');
flushSync(() => {
// This test would result in an infinite loop, so if this doesn't error, then the test is working.
});
}
});

@ -0,0 +1,9 @@
<script>
import Item from './Item.svelte';
var items = Array(1000);
</script>
{#each items as item}
<Item />
{/each}
Loading…
Cancel
Save