chore: apply each block controlled teardown optimization, again (#11051)

pull/11054/head
Dominic Gannaway 9 months ago committed by GitHub
parent f7c8fd569b
commit 02441c6a19
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194

@ -15,9 +15,11 @@ import { untrack } from '../../runtime.js';
import { import {
block, block,
branch, branch,
destroy_effect,
effect, effect,
run_out_transitions,
pause_children,
pause_effect, pause_effect,
pause_effects,
resume_effect resume_effect
} from '../../reactivity/effects.js'; } from '../../reactivity/effects.js';
import { source, mutable_source, set } from '../../reactivity/sources.js'; import { source, mutable_source, set } from '../../reactivity/sources.js';
@ -39,6 +41,39 @@ export function set_current_each_item(item) {
current_each_item = item; current_each_item = item;
} }
/**
* Pause multiple effects simultaneously, and coordinate their
* subsequent destruction. Used in each blocks
* @param {import('#client').Effect[]} effects
* @param {null | Node} controlled_anchor
* @param {() => void} [callback]
*/
function pause_effects(effects, controlled_anchor, callback) {
/** @type {import('#client').TransitionManager[]} */
var transitions = [];
var length = effects.length;
for (var i = 0; i < length; i++) {
pause_children(effects[i], transitions, true);
}
// If we have a controlled anchor, it means that the each block is inside a single
// DOM element, so we can apply a fast-path for clearing the contents of the element.
if (effects.length > 0 && transitions.length === 0 && controlled_anchor !== null) {
var parent_node = /** @type {Element} */ (controlled_anchor.parentNode);
parent_node.textContent = '';
parent_node.append(controlled_anchor);
}
run_out_transitions(transitions, () => {
for (var i = 0; i < length; i++) {
destroy_effect(effects[i]);
}
if (callback !== undefined) callback();
});
}
/** /**
* @template V * @template V
* @param {Element | Comment} anchor The next sibling node, or the parent node if this is a 'controlled' block * @param {Element | Comment} anchor The next sibling node, or the parent node if this is a 'controlled' block
@ -145,7 +180,6 @@ function each(anchor, flags, get_collection, get_key, render_fn, fallback_fn, re
} }
if (!hydrating) { if (!hydrating) {
// TODO add 'empty controlled block' optimisation here
reconcile_fn(array, state, anchor, render_fn, flags, keys); reconcile_fn(array, state, anchor, render_fn, flags, keys);
} }
@ -244,7 +278,9 @@ function reconcile_indexed_array(array, state, anchor, render_fn, flags) {
effects.push(a_items[i].e); effects.push(a_items[i].e);
} }
pause_effects(effects, () => { var controlled_anchor = (flags & EACH_IS_CONTROLLED) !== 0 && b === 0 ? anchor : null;
pause_effects(effects, controlled_anchor, () => {
state.items.length = b; state.items.length = b;
}); });
} }
@ -274,6 +310,7 @@ function reconcile_tracked_array(array, state, anchor, render_fn, flags, keys) {
var is_animated = (flags & EACH_IS_ANIMATED) !== 0; var is_animated = (flags & EACH_IS_ANIMATED) !== 0;
var should_update = (flags & (EACH_ITEM_REACTIVE | EACH_INDEX_REACTIVE)) !== 0; var should_update = (flags & (EACH_ITEM_REACTIVE | EACH_INDEX_REACTIVE)) !== 0;
var is_controlled = (flags & EACH_IS_CONTROLLED) !== 0;
var start = 0; var start = 0;
var item; var item;
@ -381,6 +418,11 @@ function reconcile_tracked_array(array, state, anchor, render_fn, flags, keys) {
// I fully understand this part) // I fully understand this part)
if (moved) { if (moved) {
mark_lis(sources); mark_lis(sources);
} else if (is_controlled && to_destroy.length === a_items.length) {
// We can optimize the case in which all items are replaced —
// destroy everything first, then append new items
pause_effects(to_destroy, anchor);
to_destroy = [];
} }
// working from the back, insert new or moved items // working from the back, insert new or moved items
@ -421,9 +463,9 @@ function reconcile_tracked_array(array, state, anchor, render_fn, flags, keys) {
}); });
} }
// TODO: would be good to avoid this closure in the case where we have no var controlled_anchor = is_controlled && b_items.length === 0 ? anchor : null;
// transitions at all. It would make it far more JIT friendly in the hot cases.
pause_effects(to_destroy, () => { pause_effects(to_destroy, controlled_anchor, () => {
state.items = b_items; state.items = b_items;
}); });
} }

@ -27,7 +27,6 @@ import {
IS_ELSEIF IS_ELSEIF
} from '../constants.js'; } from '../constants.js';
import { set } from './sources.js'; import { set } from './sources.js';
import { noop } from '../../shared/utils.js';
import { remove } from '../dom/reconciler.js'; import { remove } from '../dom/reconciler.js';
/** /**
@ -295,42 +294,17 @@ export function destroy_effect(effect) {
* completed, and if the state change is reversed then we _resume_ it. * completed, and if the state change is reversed then we _resume_ it.
* A paused effect does not update, and the DOM subtree becomes inert. * A paused effect does not update, and the DOM subtree becomes inert.
* @param {import('#client').Effect} effect * @param {import('#client').Effect} effect
* @param {() => void} callback * @param {() => void} [callback]
*/ */
export function pause_effect(effect, callback = noop) { export function pause_effect(effect, callback) {
/** @type {import('#client').TransitionManager[]} */ /** @type {import('#client').TransitionManager[]} */
var transitions = []; var transitions = [];
pause_children(effect, transitions, true); pause_children(effect, transitions, true);
out(transitions, () => { run_out_transitions(transitions, () => {
destroy_effect(effect); destroy_effect(effect);
callback(); if (callback) callback();
});
}
/**
* Pause multiple effects simultaneously, and coordinate their
* subsequent destruction. Used in each blocks
* @param {import('#client').Effect[]} effects
* @param {() => void} callback
*/
export function pause_effects(effects, callback = noop) {
/** @type {import('#client').TransitionManager[]} */
var transitions = [];
var length = effects.length;
for (var i = 0; i < length; i++) {
pause_children(effects[i], transitions, true);
}
// TODO: would be good to avoid this closure in the case where we have no
// transitions at all. It would make it far more JIT friendly in the hot cases.
out(transitions, () => {
for (var i = 0; i < length; i++) {
destroy_effect(effects[i]);
}
callback();
}); });
} }
@ -338,7 +312,7 @@ export function pause_effects(effects, callback = noop) {
* @param {import('#client').TransitionManager[]} transitions * @param {import('#client').TransitionManager[]} transitions
* @param {() => void} fn * @param {() => void} fn
*/ */
function out(transitions, fn) { export function run_out_transitions(transitions, fn) {
var remaining = transitions.length; var remaining = transitions.length;
if (remaining > 0) { if (remaining > 0) {
var check = () => --remaining || fn(); var check = () => --remaining || fn();
@ -355,7 +329,7 @@ function out(transitions, fn) {
* @param {import('#client').TransitionManager[]} transitions * @param {import('#client').TransitionManager[]} transitions
* @param {boolean} local * @param {boolean} local
*/ */
function pause_children(effect, transitions, local) { export function pause_children(effect, transitions, local) {
if ((effect.f & INERT) !== 0) return; if ((effect.f & INERT) !== 0) return;
effect.f ^= INERT; effect.f ^= INERT;

Loading…
Cancel
Save