diff --git a/packages/svelte/src/internal/client/dom/blocks/each.js b/packages/svelte/src/internal/client/dom/blocks/each.js index 8c59e3aebab3..baf86a2c9516 100644 --- a/packages/svelte/src/internal/client/dom/blocks/each.js +++ b/packages/svelte/src/internal/client/dom/blocks/each.js @@ -10,13 +10,12 @@ import { } from '../../../../constants.js'; import { hydrate_anchor, - hydrate_nodes, hydrate_start, hydrating, + remove_hydrate_nodes, set_hydrating } from '../hydration.js'; import { clear_text_content, empty } from '../operations.js'; -import { remove } from '../reconciler.js'; import { untrack } from '../../runtime.js'; import { block, @@ -145,7 +144,7 @@ export function each(anchor, flags, get_collection, get_key, render_fn, fallback if (is_else !== (length === 0)) { // hydration mismatch — remove the server-rendered DOM and start over - remove(hydrate_nodes); + remove_hydrate_nodes(); set_hydrating(false); mismatch = true; } diff --git a/packages/svelte/src/internal/client/dom/blocks/html.js b/packages/svelte/src/internal/client/dom/blocks/html.js index 103bf7c046db..98fb91c41bfd 100644 --- a/packages/svelte/src/internal/client/dom/blocks/html.js +++ b/packages/svelte/src/internal/client/dom/blocks/html.js @@ -1,29 +1,9 @@ import { derived } from '../../reactivity/deriveds.js'; import { render_effect } from '../../reactivity/effects.js'; -import { current_effect, get } from '../../runtime.js'; -import { is_array } from '../../utils.js'; -import { hydrate_nodes, hydrating } from '../hydration.js'; -import { create_fragment_from_html, remove } from '../reconciler.js'; - -/** - * @param {import('#client').Effect} effect - * @param {(Element | Comment | Text)[]} to_remove - * @returns {void} - */ -function remove_from_parent_effect(effect, to_remove) { - const dom = effect.dom; - - if (is_array(dom)) { - for (let i = dom.length - 1; i >= 0; i--) { - if (to_remove.includes(dom[i])) { - dom.splice(i, 1); - break; - } - } - } else if (dom !== null && to_remove.includes(dom)) { - effect.dom = null; - } -} +import { get } from '../../runtime.js'; +import { hydrate_start, hydrating } from '../hydration.js'; +import { remove_nodes } from '../operations.js'; +import { create_fragment_from_html } from '../reconciler.js'; /** * @param {Element | Text | Comment} anchor @@ -33,20 +13,14 @@ function remove_from_parent_effect(effect, to_remove) { * @returns {void} */ export function html(anchor, get_value, svg, mathml) { - const parent_effect = anchor.parentNode !== current_effect?.dom ? current_effect : null; let value = derived(get_value); render_effect(() => { - var dom = html_to_dom(anchor, get(value), svg, mathml); + var [start, end] = html_to_dom(anchor, get(value), svg, mathml); - if (dom) { - return () => { - if (parent_effect !== null) { - remove_from_parent_effect(parent_effect, is_array(dom) ? dom : [dom]); - } - remove(dom); - }; - } + return () => { + remove_nodes(start, end); + }; }); } @@ -58,10 +32,12 @@ export function html(anchor, get_value, svg, mathml) { * @param {V} value * @param {boolean} svg * @param {boolean} mathml - * @returns {Element | Comment | (Element | Comment | Text)[]} + * @returns {[import('#client').TemplateNode, import('#client').TemplateNode]} */ function html_to_dom(target, value, svg, mathml) { - if (hydrating) return hydrate_nodes; + if (hydrating) { + return [hydrate_start, hydrate_start]; + } var html = value + ''; if (svg) html = `${html}`; @@ -79,10 +55,11 @@ function html_to_dom(target, value, svg, mathml) { if (node.childNodes.length === 1) { var child = /** @type {Text | Element | Comment} */ (node.firstChild); target.before(child); - return child; + return [child, child]; } - var nodes = /** @type {Array} */ ([...node.childNodes]); + var first = /** @type {import('#client').TemplateNode} */ (node.firstChild); + var last = /** @type {import('#client').TemplateNode} */ (node.lastChild); if (svg || mathml) { while (node.firstChild) { @@ -92,5 +69,5 @@ function html_to_dom(target, value, svg, mathml) { target.before(node); } - return nodes; + return [first, last]; } diff --git a/packages/svelte/src/internal/client/dom/blocks/if.js b/packages/svelte/src/internal/client/dom/blocks/if.js index 80ed8c09f4f6..41c26bbfd8b0 100644 --- a/packages/svelte/src/internal/client/dom/blocks/if.js +++ b/packages/svelte/src/internal/client/dom/blocks/if.js @@ -1,6 +1,5 @@ import { EFFECT_TRANSPARENT } from '../../constants.js'; -import { hydrate_nodes, hydrating, set_hydrating } from '../hydration.js'; -import { remove } from '../reconciler.js'; +import { hydrating, remove_hydrate_nodes, set_hydrating } from '../hydration.js'; import { block, branch, pause_effect, resume_effect } from '../../reactivity/effects.js'; import { HYDRATION_END_ELSE } from '../../../../constants.js'; @@ -42,7 +41,7 @@ export function if_block( if (condition === is_else) { // Hydration mismatch: remove everything inside the anchor and start fresh. // This could happen with `{#if browser}...{/if}`, for example - remove(hydrate_nodes); + remove_hydrate_nodes(); set_hydrating(false); mismatch = true; } diff --git a/packages/svelte/src/internal/client/dom/blocks/svelte-head.js b/packages/svelte/src/internal/client/dom/blocks/svelte-head.js index b00a3a242b1e..e41dfc120b57 100644 --- a/packages/svelte/src/internal/client/dom/blocks/svelte-head.js +++ b/packages/svelte/src/internal/client/dom/blocks/svelte-head.js @@ -1,7 +1,7 @@ -import { hydrate_anchor, hydrate_nodes, hydrating, set_hydrate_nodes } from '../hydration.js'; +import { hydrate_anchor, hydrate_start, hydrating, set_hydrate_nodes } from '../hydration.js'; import { empty } from '../operations.js'; import { block } from '../../reactivity/effects.js'; -import { HYDRATION_END, HYDRATION_START } from '../../../../constants.js'; +import { HYDRATION_START } from '../../../../constants.js'; /** * @type {Node | undefined} @@ -19,14 +19,14 @@ export function reset_head_anchor() { export function head(render_fn) { // The head function may be called after the first hydration pass and ssr comment nodes may still be present, // therefore we need to skip that when we detect that we're not in hydration mode. - let previous_hydrate_nodes = null; + let previous_hydrate_start = null; let was_hydrating = hydrating; /** @type {Comment | Text} */ var anchor; if (hydrating) { - previous_hydrate_nodes = hydrate_nodes; + previous_hydrate_start = hydrate_start; // There might be multiple head blocks in our app, so we need to account for each one needing independent hydration. if (head_anchor === undefined) { @@ -50,7 +50,7 @@ export function head(render_fn) { block(() => render_fn(anchor)); } finally { if (was_hydrating) { - set_hydrate_nodes(/** @type {import('#client').TemplateNode[]} */ (previous_hydrate_nodes)); + set_hydrate_nodes(/** @type {import('#client').TemplateNode} */ (previous_hydrate_start)); } } } diff --git a/packages/svelte/src/internal/client/dom/hydration.js b/packages/svelte/src/internal/client/dom/hydration.js index 06113ba784da..d51ae78ca362 100644 --- a/packages/svelte/src/internal/client/dom/hydration.js +++ b/packages/svelte/src/internal/client/dom/hydration.js @@ -1,6 +1,7 @@ import { DEV } from 'esm-env'; import { HYDRATION_END, HYDRATION_START, HYDRATION_ERROR } from '../../../constants.js'; import * as w from '../warnings.js'; +import { remove_nodes } from './operations.js'; /** * Use this variable to guard everything related to hydration code so it can be treeshaken out @@ -13,47 +14,36 @@ export function set_hydrating(value) { hydrating = value; } -/** - * Array of nodes to traverse for hydration. This will be null if we're not hydrating, but for - * the sake of simplicity we're not going to use `null` checks everywhere and instead rely on - * the `hydrating` flag to tell whether or not we're in hydration mode at which point this is set. - * @type {import('#client').TemplateNode[]} - */ -export let hydrate_nodes = /** @type {any} */ (null); - /** @type {import('#client').TemplateNode} */ -export let hydrate_start; +export let hydrate_start = /** @type {any} */ (null); -/** @param {import('#client').TemplateNode[]} nodes */ -export function set_hydrate_nodes(nodes) { - hydrate_nodes = nodes; - hydrate_start = nodes && nodes[0]; +/** + * @param {import('#client').TemplateNode} start + */ +export function set_hydrate_nodes(start) { + hydrate_start = start; } /** * This function is only called when `hydrating` is true. If passed a `` opening - * hydration marker, it finds the corresponding closing marker and sets `hydrate_nodes` - * to everything between the markers, before returning the closing marker. + * hydration marker, it sets `hydrate_start` to be the next node and returns the closing marker * @param {Node} node * @returns {Node} */ export function hydrate_anchor(node) { - if (node.nodeType !== 8) { - return node; - } - - var current = /** @type {Node | null} */ (node); - // TODO this could have false positives, if a user comment consisted of `[`. need to tighten that up - if (/** @type {Comment} */ (current).data !== HYDRATION_START) { + if (node.nodeType !== 8 || /** @type {Comment} */ (node).data !== HYDRATION_START) { return node; } - /** @type {Node[]} */ - var nodes = []; + hydrate_start = /** @type {import('#client').TemplateNode} */ ( + /** @type {Comment} */ (node).nextSibling + ); + + var current = hydrate_start; var depth = 0; - while ((current = /** @type {Node} */ (current).nextSibling) !== null) { + while (current !== null) { if (current.nodeType === 8) { var data = /** @type {Comment} */ (current).data; @@ -61,8 +51,6 @@ export function hydrate_anchor(node) { depth += 1; } else if (data[0] === HYDRATION_END) { if (depth === 0) { - hydrate_nodes = /** @type {import('#client').TemplateNode[]} */ (nodes); - hydrate_start = /** @type {import('#client').TemplateNode} */ (nodes[0]); return current; } @@ -70,7 +58,7 @@ export function hydrate_anchor(node) { } } - nodes.push(current); + current = /** @type {import('#client').TemplateNode} */ (current.nextSibling); } let location; @@ -86,3 +74,26 @@ export function hydrate_anchor(node) { w.hydration_mismatch(location); throw HYDRATION_ERROR; } + +export function remove_hydrate_nodes() { + /** @type {import('#client').TemplateNode | null} */ + var node = hydrate_start; + var depth = 0; + + while (node) { + if (node.nodeType === 8) { + var data = /** @type {Comment} */ (node).data; + + if (data === HYDRATION_START) { + depth += 1; + } else if (data[0] === HYDRATION_END) { + if (depth === 0) return; + depth -= 1; + } + } + + var next = /** @type {import('#client').TemplateNode | null} */ (node.nextSibling); + node.remove(); + node = next; + } +} diff --git a/packages/svelte/src/internal/client/dom/operations.js b/packages/svelte/src/internal/client/dom/operations.js index 508ea47388e1..7fb84aa44c88 100644 --- a/packages/svelte/src/internal/client/dom/operations.js +++ b/packages/svelte/src/internal/client/dom/operations.js @@ -1,7 +1,6 @@ import { hydrate_anchor, hydrate_start, hydrating } from './hydration.js'; import { DEV } from 'esm-env'; import { init_array_prototype_warnings } from '../dev/equality.js'; -import { current_effect } from '../runtime.js'; // export these for reference in the compiled code, making global name deduplication unnecessary /** @type {Window} */ @@ -69,7 +68,7 @@ export function child(node) { } /** - * @param {DocumentFragment | import('#client').TemplateNode[]} fragment + * @param {DocumentFragment} fragment * @param {boolean} is_text * @returns {Node | null} */ @@ -83,14 +82,8 @@ export function first_child(fragment, is_text) { // if an {expression} is empty during SSR, there might be no // text node to hydrate — we must therefore create one if (is_text && hydrate_start?.nodeType !== 3) { - var text = empty(); - var dom = /** @type {import('#client').TemplateNode[]} */ ( - /** @type {import('#client').Effect} */ (current_effect).dom - ); - - dom.unshift(text); + const text = empty(); hydrate_start?.before(text); - return text; } @@ -114,14 +107,8 @@ export function sibling(node, is_text = false) { // if a sibling {expression} is empty during SSR, there might be no // text node to hydrate — we must therefore create one if (is_text && next_sibling?.nodeType !== 3) { - var text = empty(); - var dom = /** @type {import('#client').TemplateNode[]} */ ( - /** @type {import('#client').Effect} */ (current_effect).dom - ); - - dom.unshift(text); + const text = empty(); next_sibling?.before(text); - return text; } @@ -142,3 +129,21 @@ export function clear_text_content(node) { export function create_element(name) { return document.createElement(name); } + +/** + * Remove all nodes between `from` and `to`, inclusive + * @param {import('#client').TemplateNode} from + * @param {import('#client').TemplateNode} to + */ +export function remove_nodes(from, to) { + var node = from; + + while (node) { + var next = node.nextSibling; + + node.remove(); + if (node === to) break; + + node = /** @type {import('#client').TemplateNode} */ (next); + } +} diff --git a/packages/svelte/src/internal/client/dom/reconciler.js b/packages/svelte/src/internal/client/dom/reconciler.js index 5b9f246ed338..de9b2db49d83 100644 --- a/packages/svelte/src/internal/client/dom/reconciler.js +++ b/packages/svelte/src/internal/client/dom/reconciler.js @@ -6,19 +6,3 @@ export function create_fragment_from_html(html) { elem.innerHTML = html; return elem.content; } - -/** - * @param {import('#client').Dom} current - */ -export function remove(current) { - if (is_array(current)) { - for (var i = 0; i < current.length; i++) { - var node = current[i]; - if (node.isConnected) { - node.remove(); - } - } - } else if (current.isConnected) { - current.remove(); - } -} diff --git a/packages/svelte/src/internal/client/dom/template.js b/packages/svelte/src/internal/client/dom/template.js index 6458dc65cddf..73c4e98be9bb 100644 --- a/packages/svelte/src/internal/client/dom/template.js +++ b/packages/svelte/src/internal/client/dom/template.js @@ -1,4 +1,4 @@ -import { hydrate_nodes, hydrate_start, hydrating } from './hydration.js'; +import { hydrate_start, hydrating } from './hydration.js'; import { empty } from './operations.js'; import { create_fragment_from_html } from './reconciler.js'; import { current_effect } from '../runtime.js'; @@ -6,14 +6,14 @@ import { TEMPLATE_FRAGMENT, TEMPLATE_USE_IMPORT_NODE } from '../../../constants. import { effect } from '../reactivity/effects.js'; /** - * @template {import("#client").TemplateNode | import("#client").TemplateNode[]} T - * @param {T} dom + * @template {import("#client").TemplateNode} T + * @param {T} d1 */ -function push_template_node(dom) { +function push_template_node(d1) { var effect = /** @type {import('#client').Effect} */ (current_effect); - if (effect.dom === null) { - effect.dom = dom; + if (effect.d1 === null) { + effect.d1 = d1; } } @@ -32,7 +32,7 @@ export function template(content, flags) { return () => { if (hydrating) { - push_template_node(is_fragment ? hydrate_nodes : hydrate_start); + push_template_node(hydrate_start); return hydrate_start; } @@ -77,7 +77,6 @@ export function template_with_script(content, flags) { */ /*#__NO_SIDE_EFFECTS__*/ export function ns_template(content, flags, ns = 'svg') { - var is_fragment = (flags & TEMPLATE_FRAGMENT) !== 0; var fn = template(`<${ns}>${content}`, 0); // we don't need to worry about using importNode for namespaced elements /** @type {Element | DocumentFragment} */ @@ -85,7 +84,7 @@ export function ns_template(content, flags, ns = 'svg') { return () => { if (hydrating) { - push_template_node(is_fragment ? hydrate_nodes : hydrate_start); + push_template_node(hydrate_start); return hydrate_start; } @@ -182,7 +181,7 @@ export function text(anchor) { var node = hydrate_start; if (!node) { - // if an {expression} is empty during SSR, `hydrate_nodes` will be empty. + // if an {expression} is empty during SSR, `hydrate_start` will be missing. // we need to insert an empty text node anchor.before((node = empty())); } @@ -194,7 +193,7 @@ export function text(anchor) { export function comment() { // we're not delegating to `template` here for performance reasons if (hydrating) { - push_template_node(hydrate_nodes); + push_template_node(hydrate_start); return hydrate_start; } @@ -212,14 +211,21 @@ export function comment() { * @param {DocumentFragment | Element} dom */ export function append(anchor, dom) { - if (hydrating) return; - var effect = /** @type {import('#client').Effect} */ (current_effect); - effect.dom = - dom.nodeType === 11 - ? /** @type {import('#client').TemplateNode[]} */ ([...dom.childNodes]) - : /** @type {Element | Comment} */ (dom); + if (!hydrating) { + if (dom.nodeType === 11) { + // prepend an empty text node + var d1 = empty(); + + /** @type {import('#client').TemplateNode} */ (dom.firstChild).before(d1); + effect.d1 = d1; + } else { + effect.d1 = /** @type {Element} */ (dom); + } + + anchor.before(/** @type {Node} */ (dom)); + } - anchor.before(/** @type {Node} */ (dom)); + effect.d2 = anchor?.previousSibling; } diff --git a/packages/svelte/src/internal/client/reactivity/effects.js b/packages/svelte/src/internal/client/reactivity/effects.js index 6411e8109af7..0554271fc44b 100644 --- a/packages/svelte/src/internal/client/reactivity/effects.js +++ b/packages/svelte/src/internal/client/reactivity/effects.js @@ -33,10 +33,10 @@ import { UNOWNED } from '../constants.js'; import { set } from './sources.js'; -import { remove } from '../dom/reconciler.js'; import * as e from '../errors.js'; import { DEV } from 'esm-env'; import { define_property } from '../utils.js'; +import { remove_nodes } from '../dom/operations.js'; /** * @param {'$effect' | '$effect.pre' | '$inspect'} rune @@ -79,7 +79,8 @@ function create_effect(type, fn, sync) { var effect = { ctx: current_component_context, deps: null, - dom: null, + d1: null, + d2: null, f: type | DIRTY, first: null, fn, @@ -314,10 +315,11 @@ export function execute_effect_teardown(effect) { * @returns {void} */ export function destroy_effect(effect) { - var dom = effect.dom; + var d1 = effect.d1; + var d2 = effect.d2; - if (dom !== null) { - remove(dom); + if (d1 !== null && d2 !== null) { + remove_nodes(d1, d2); } destroy_effect_children(effect); @@ -360,7 +362,8 @@ export function destroy_effect(effect) { effect.prev = effect.teardown = effect.ctx = - effect.dom = + effect.d1 = + effect.d2 = effect.deps = effect.parent = // @ts-expect-error diff --git a/packages/svelte/src/internal/client/reactivity/types.d.ts b/packages/svelte/src/internal/client/reactivity/types.d.ts index 7502443bd10c..eef3c3a2e3e2 100644 --- a/packages/svelte/src/internal/client/reactivity/types.d.ts +++ b/packages/svelte/src/internal/client/reactivity/types.d.ts @@ -1,4 +1,4 @@ -import type { ComponentContext, Dom, Equals, TransitionManager } from '#client'; +import type { ComponentContext, Dom, Equals, TemplateNode, TransitionManager } from '#client'; export interface Signal { /** Flags bitmask */ @@ -36,7 +36,8 @@ export interface Derived extends Value, Reaction { export interface Effect extends Reaction { parent: Effect | null; - dom: Dom | null; + d1: TemplateNode | null; + d2: TemplateNode | null; /** The associated component context */ ctx: null | ComponentContext; /** The effect function */ diff --git a/packages/svelte/src/internal/client/render.js b/packages/svelte/src/internal/client/render.js index 101ba6f1afd3..2117113c555d 100644 --- a/packages/svelte/src/internal/client/render.js +++ b/packages/svelte/src/internal/client/render.js @@ -5,7 +5,7 @@ import { flush_sync, push, pop, current_component_context } from './runtime.js'; import { effect_root, branch } from './reactivity/effects.js'; import { hydrate_anchor, - hydrate_nodes, + hydrate_start, hydrating, set_hydrate_nodes, set_hydrating @@ -129,7 +129,7 @@ export function hydrate(component, options) { } const target = options.target; - const previous_hydrate_nodes = hydrate_nodes; + const previous_hydrate_start = hydrate_start; try { // Don't flush previous effects to ensure order of outer effects stays consistent @@ -173,8 +173,8 @@ export function hydrate(component, options) { throw error; } finally { - set_hydrating(!!previous_hydrate_nodes); - set_hydrate_nodes(previous_hydrate_nodes); + set_hydrating(!!previous_hydrate_start); + set_hydrate_nodes(previous_hydrate_start); reset_head_anchor(); } }