feat: defer tasks without creating effects (#11960)

In a handful of places, we're using effect(...) to defer work that only needs to happen once (like autofocusing an element). This is overkill. There is a much cheaper and simpler way that already exists in the codebase — queue_micro_task.

It feels like we could probably use it in more places, but when I tried it causes some tests to fail, likely because of subtle timing issues that don't apply to the things changed in this PR.
pull/11985/head
Rich Harris 1 year ago committed by GitHub
parent 36a5143758
commit 2be6d43ea3
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194

@ -0,0 +1,5 @@
---
'svelte': patch
---
feat: defer tasks without creating effects

@ -17,12 +17,10 @@ import {
} from '../hydration.js'; } from '../hydration.js';
import { clear_text_content, empty } from '../operations.js'; import { clear_text_content, empty } from '../operations.js';
import { remove } from '../reconciler.js'; import { remove } from '../reconciler.js';
import { untrack } from '../../runtime.js';
import { import {
block, block,
branch, branch,
destroy_effect, destroy_effect,
effect,
run_out_transitions, run_out_transitions,
pause_children, pause_children,
pause_effect, pause_effect,
@ -31,6 +29,7 @@ import {
import { source, mutable_source, set } from '../../reactivity/sources.js'; import { source, mutable_source, set } from '../../reactivity/sources.js';
import { is_array, is_frozen } from '../../utils.js'; import { is_array, is_frozen } from '../../utils.js';
import { INERT, STATE_SYMBOL } from '../../constants.js'; import { INERT, STATE_SYMBOL } from '../../constants.js';
import { queue_micro_task } from '../task.js';
/** /**
* The row of a keyed each block that is currently updating. We track this * The row of a keyed each block that is currently updating. We track this
@ -423,12 +422,10 @@ function reconcile(array, state, anchor, render_fn, flags, get_key) {
} }
if (is_animated) { if (is_animated) {
effect(() => { queue_micro_task(() => {
untrack(() => { for (item of to_animate) {
for (item of to_animate) { item.a?.apply();
item.a?.apply(); }
}
});
}); });
} }
} }

@ -9,10 +9,9 @@ import {
} from '../../../../constants.js'; } from '../../../../constants.js';
import { create_event, delegate } from './events.js'; import { create_event, delegate } from './events.js';
import { add_form_reset_listener, autofocus } from './misc.js'; import { add_form_reset_listener, autofocus } from './misc.js';
import { effect, effect_root } from '../../reactivity/effects.js';
import * as w from '../../warnings.js'; import * as w from '../../warnings.js';
import { LOADING_ATTR_SYMBOL } from '../../constants.js'; import { LOADING_ATTR_SYMBOL } from '../../constants.js';
import { queue_idle_task } from '../task.js'; import { queue_idle_task, queue_micro_task } from '../task.js';
/** /**
* The value/checked attribute in the template actually corresponds to the defaultValue property, so we need * The value/checked attribute in the template actually corresponds to the defaultValue property, so we need
@ -262,21 +261,13 @@ export function set_attributes(element, prev, next, lowercase_attributes, css_ha
// On the first run, ensure that events are added after bindings so // On the first run, ensure that events are added after bindings so
// that their listeners fire after the binding listeners // that their listeners fire after the binding listeners
if (!prev) { if (!prev) {
// In edge cases it may happen that set_attributes is re-run before the queue_micro_task(() => {
// effect is executed. In that case the render effect which initiates this if (!element.isConnected) return;
// re-run will destroy the inner effect and it will never run. But because for (const [key, value, evt] of events) {
// next and prev may have the same keys, the event would not get added again if (current[key] === value) {
// and it would get lost. We prevent this by using a root effect. evt();
const destroy_root = effect_root(() => {
effect(() => {
if (!element.isConnected) return;
for (const [key, value, evt] of events) {
if (current[key] === value) {
evt();
}
} }
destroy_root(); }
});
}); });
} }

@ -1,8 +1,9 @@
import { DEV } from 'esm-env'; import { DEV } from 'esm-env';
import { render_effect, effect, teardown } from '../../../reactivity/effects.js'; import { render_effect, teardown } from '../../../reactivity/effects.js';
import { listen_to_event_and_reset_event } from './shared.js'; import { listen_to_event_and_reset_event } from './shared.js';
import * as e from '../../../errors.js'; import * as e from '../../../errors.js';
import { get_proxied_value, is } from '../../../proxy.js'; import { get_proxied_value, is } from '../../../proxy.js';
import { queue_micro_task } from '../../task.js';
/** /**
* @param {HTMLInputElement} input * @param {HTMLInputElement} input
@ -111,7 +112,7 @@ export function bind_group(inputs, group_index, input, get_value, update) {
} }
}); });
effect(() => { queue_micro_task(() => {
// necessary to maintain binding group order in all insertion scenarios. TODO optimise // necessary to maintain binding group order in all insertion scenarios. TODO optimise
binding_group.sort((a, b) => (a.compareDocumentPosition(b) === 4 ? -1 : 1)); binding_group.sort((a, b) => (a.compareDocumentPosition(b) === 4 ? -1 : 1));
}); });

@ -1,4 +1,4 @@
import { render_effect, teardown } from '../../reactivity/effects.js'; import { teardown } from '../../reactivity/effects.js';
import { all_registered_events, root_event_handles } from '../../render.js'; import { all_registered_events, root_event_handles } from '../../render.js';
import { define_property, is_array } from '../../utils.js'; import { define_property, is_array } from '../../utils.js';
import { hydrating } from '../hydration.js'; import { hydrating } from '../hydration.js';

@ -1,6 +1,6 @@
import { hydrating } from '../hydration.js'; import { hydrating } from '../hydration.js';
import { effect } from '../../reactivity/effects.js';
import { clear_text_content } from '../operations.js'; import { clear_text_content } from '../operations.js';
import { queue_micro_task } from '../task.js';
/** /**
* @param {HTMLElement} dom * @param {HTMLElement} dom
@ -12,7 +12,7 @@ export function autofocus(dom, value) {
const body = document.body; const body = document.body;
dom.autofocus = true; dom.autofocus = true;
effect(() => { queue_micro_task(() => {
if (document.activeElement === body) { if (document.activeElement === body) {
dom.focus(); dom.focus();
} }

@ -8,6 +8,7 @@ import { is_function } from '../../utils.js';
import { current_each_item } from '../blocks/each.js'; import { current_each_item } from '../blocks/each.js';
import { TRANSITION_GLOBAL, TRANSITION_IN, TRANSITION_OUT } from '../../../../constants.js'; import { TRANSITION_GLOBAL, TRANSITION_IN, TRANSITION_OUT } from '../../../../constants.js';
import { BLOCK_EFFECT, EFFECT_RAN, EFFECT_TRANSPARENT } from '../../constants.js'; import { BLOCK_EFFECT, EFFECT_RAN, EFFECT_TRANSPARENT } from '../../constants.js';
import { queue_micro_task } from '../task.js';
/** /**
* @param {Element} element * @param {Element} element
@ -272,8 +273,8 @@ function animate(element, options, counterpart, t2, callback) {
/** @type {import('#client').Animation} */ /** @type {import('#client').Animation} */
var a; var a;
effect(() => { queue_micro_task(() => {
var o = untrack(() => options({ direction: t2 === 1 ? 'in' : 'out' })); var o = options({ direction: t2 === 1 ? 'in' : 'out' });
a = animate(element, o, counterpart, t2, callback); a = animate(element, o, counterpart, t2, callback);
}); });

@ -3,8 +3,8 @@ import { empty } from './operations.js';
import { create_fragment_from_html } from './reconciler.js'; import { create_fragment_from_html } from './reconciler.js';
import { current_effect } from '../runtime.js'; import { current_effect } from '../runtime.js';
import { TEMPLATE_FRAGMENT, TEMPLATE_USE_IMPORT_NODE } from '../../../constants.js'; import { TEMPLATE_FRAGMENT, TEMPLATE_USE_IMPORT_NODE } from '../../../constants.js';
import { effect } from '../reactivity/effects.js';
import { is_array } from '../utils.js'; import { is_array } from '../utils.js';
import { queue_micro_task } from './task.js';
/** /**
* @template {import("#client").TemplateNode | import("#client").TemplateNode[]} T * @template {import("#client").TemplateNode | import("#client").TemplateNode[]} T
@ -196,7 +196,7 @@ function run_scripts(node) {
// Don't do it in other circumstances or we could accidentally execute scripts // Don't do it in other circumstances or we could accidentally execute scripts
// in an adjacent @html tag that was instantiated in the meantime. // in an adjacent @html tag that was instantiated in the meantime.
if (script === node) { if (script === node) {
effect(() => script.replaceWith(clone)); queue_micro_task(() => script.replaceWith(clone));
} else { } else {
script.replaceWith(clone); script.replaceWith(clone);
} }

@ -561,7 +561,7 @@ function infinite_loop_guard() {
* @returns {void} * @returns {void}
*/ */
function flush_queued_root_effects(root_effects) { function flush_queued_root_effects(root_effects) {
const length = root_effects.length; var length = root_effects.length;
if (length === 0) { if (length === 0) {
return; return;
} }

Loading…
Cancel
Save