chore: tidy up hydration code (#10891)

* remove some indirection

* tidy up

* tidy

* tidy up

* simplify

* fix

* don't attempt to hydrate children of void dynamic element

* simplify

* tighten up

* fix

* add note, simplify

* tidy up

* changeset

* revert this change, save for a separate PR
pull/10893/head
Rich Harris 6 months ago committed by GitHub
parent 7f10642add
commit dfd1819559
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194

@ -1,5 +1,5 @@
import { namespace_svg } from '../../../../constants.js';
import { current_hydration_fragment, hydrate_block_anchor, hydrating } from '../hydration.js';
import { hydrate_nodes, hydrate_block_anchor, hydrating } from '../hydration.js';
import { empty } from '../operations.js';
import { render_effect } from '../../reactivity/effects.js';
import { remove } from '../reconciler.js';
@ -22,7 +22,7 @@ export function css_props(anchor, is_html, props, component) {
if (hydrating) {
// Hydration: css props element is surrounded by a ssr comment ...
tag = /** @type {HTMLElement | SVGElement} */ (current_hydration_fragment[0]);
tag = /** @type {HTMLElement | SVGElement} */ (hydrate_nodes[0]);
// ... and the child(ren) of the css props element is also surround by a ssr comment
component_anchor = /** @type {Comment} */ (tag.firstChild);
} else {

@ -7,11 +7,11 @@ import {
EACH_KEYED
} from '../../../../constants.js';
import {
current_hydration_fragment,
get_hydration_fragment,
hydrate_nodes,
hydrate_block_anchor,
hydrating,
set_current_hydration_fragment
set_hydrating,
update_hydrate_nodes
} from '../hydration.js';
import { empty } from '../operations.js';
import { insert, remove } from '../reconciler.js';
@ -98,17 +98,16 @@ function each(anchor, get_collection, flags, get_key, render_fn, fallback_fn, re
let mismatch = false;
if (hydrating) {
var is_else =
/** @type {Comment} */ (current_hydration_fragment?.[0])?.data === 'ssr:each_else';
var is_else = /** @type {Comment} */ (hydrate_nodes?.[0])?.data === 'ssr:each_else';
if (is_else !== (length === 0)) {
// hydration mismatch — remove the server-rendered DOM and start over
remove(current_hydration_fragment);
set_current_hydration_fragment(null);
remove(hydrate_nodes);
set_hydrating(false);
mismatch = true;
} else if (is_else) {
// Remove the each_else comment node or else it will confuse the subsequent hydration algorithm
/** @type {import('#client').TemplateNode[]} */ (current_hydration_fragment).shift();
/** @type {import('#client').TemplateNode[]} */ (hydrate_nodes).shift();
}
}
@ -117,18 +116,17 @@ function each(anchor, get_collection, flags, get_key, render_fn, fallback_fn, re
var b_items = [];
// Hydrate block
var hydration_list = /** @type {import('#client').TemplateNode[]} */ (
current_hydration_fragment
);
var hydration_list = /** @type {import('#client').TemplateNode[]} */ (hydrate_nodes);
var hydrating_node = hydration_list[0];
for (var i = 0; i < length; i++) {
var fragment = get_hydration_fragment(hydrating_node);
set_current_hydration_fragment(fragment);
if (!fragment) {
// If fragment is null, then that means that the server rendered less items than what
// the client code specifies -> break out and continue with client-side node creation
var nodes = update_hydrate_nodes(hydrating_node);
if (nodes === null) {
// If `nodes` is null, then that means that the server rendered fewer items than what
// expected, so break out and continue appending non-hydrated items
mismatch = true;
set_hydrating(false);
break;
}
@ -137,7 +135,7 @@ function each(anchor, get_collection, flags, get_key, render_fn, fallback_fn, re
// TODO helperise this
hydrating_node = /** @type {import('#client').TemplateNode} */ (
/** @type {Node} */ (
/** @type {Node} */ (fragment[fragment.length - 1] || hydrating_node).nextSibling
/** @type {Node} */ (nodes[nodes.length - 1] || hydrating_node).nextSibling
).nextSibling
);
}
@ -175,8 +173,8 @@ function each(anchor, get_collection, flags, get_key, render_fn, fallback_fn, re
}
if (mismatch) {
// Set a fragment so that Svelte continues to operate in hydration mode
set_current_hydration_fragment([]);
// continue in hydration mode
set_hydrating(true);
}
});

@ -1,10 +1,5 @@
import { IS_ELSEIF } from '../../constants.js';
import {
current_hydration_fragment,
hydrate_block_anchor,
hydrating,
set_current_hydration_fragment
} from '../hydration.js';
import { hydrate_nodes, hydrate_block_anchor, hydrating, set_hydrating } from '../hydration.js';
import { remove } from '../reconciler.js';
import {
destroy_effect,
@ -40,7 +35,7 @@ export function if_block(anchor, get_condition, consequent_fn, alternate_fn, els
let mismatch = false;
if (hydrating) {
const comment_text = /** @type {Comment} */ (current_hydration_fragment?.[0])?.data;
const comment_text = /** @type {Comment} */ (hydrate_nodes?.[0])?.data;
if (
!comment_text ||
@ -49,12 +44,12 @@ export function if_block(anchor, get_condition, consequent_fn, alternate_fn, els
) {
// Hydration mismatch: remove everything inside the anchor and start fresh.
// This could happen using when `{#if browser} .. {/if}` in SvelteKit.
remove(current_hydration_fragment);
set_current_hydration_fragment(null);
remove(hydrate_nodes);
set_hydrating(false);
mismatch = true;
} else {
// Remove the ssr:if comment node or else it will confuse the subsequent hydration algorithm
current_hydration_fragment.shift();
hydrate_nodes.shift();
}
}
@ -85,8 +80,8 @@ export function if_block(anchor, get_condition, consequent_fn, alternate_fn, els
}
if (mismatch) {
// Set fragment so that Svelte continues to operate in hydration mode
set_current_hydration_fragment([]);
// continue in hydration mode
set_hydrating(true);
}
});

@ -1,5 +1,5 @@
import { namespace_svg } from '../../../../constants.js';
import { current_hydration_fragment, hydrate_block_anchor, hydrating } from '../hydration.js';
import { hydrate_nodes, hydrate_block_anchor, hydrating } from '../hydration.js';
import { empty } from '../operations.js';
import {
destroy_effect,
@ -105,22 +105,25 @@ export function element(anchor, get_tag, is_svg, render_fn) {
effect = render_effect(() => {
const prev_element = element;
element = hydrating
? /** @type {Element} */ (current_hydration_fragment[0])
? /** @type {Element} */ (hydrate_nodes[0])
: ns
? document.createElementNS(ns, next_tag)
: document.createElement(next_tag);
if (render_fn) {
let anchor;
if (hydrating) {
// Use the existing ssr comment as the anchor so that the inner open and close
// methods can pick up the existing nodes correctly
anchor = /** @type {Comment} */ (element.firstChild);
} else {
anchor = empty();
element.appendChild(anchor);
// If hydrating, use the existing ssr comment as the anchor so that the
// inner open and close methods can pick up the existing nodes correctly
var child_anchor = hydrating
? /** @type {Comment} */ (element.firstChild)
: element.appendChild(empty());
if (child_anchor) {
// `child_anchor` can be undefined if this is a void element with children,
// i.e. `<svelte:element this={"hr"}>...</svelte:element>`. This is
// user error, but we warn on it elsewhere (in dev) so here we just
// silently ignore it
render_fn(element, child_anchor);
}
render_fn(element, anchor);
}
anchor.before(element);

@ -1,9 +1,4 @@
import {
current_hydration_fragment,
get_hydration_fragment,
hydrating,
set_current_hydration_fragment
} from '../hydration.js';
import { hydrate_nodes, hydrating, set_hydrate_nodes, update_hydrate_nodes } from '../hydration.js';
import { empty } from '../operations.js';
import { render_effect } from '../../reactivity/effects.js';
import { remove } from '../reconciler.js';
@ -15,14 +10,12 @@ import { remove } from '../reconciler.js';
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 hydration_fragment = null;
let previous_hydration_fragment = null;
let previous_hydrate_nodes = null;
let was_hydrating = hydrating;
let is_hydrating = hydrating;
if (is_hydrating) {
hydration_fragment = get_hydration_fragment(document.head.firstChild);
previous_hydration_fragment = current_hydration_fragment;
set_current_hydration_fragment(hydration_fragment);
if (hydrating) {
previous_hydrate_nodes = hydrate_nodes;
update_hydrate_nodes(document.head.firstChild);
}
try {
@ -50,8 +43,8 @@ export function head(render_fn) {
}
};
} finally {
if (is_hydrating) {
set_current_hydration_fragment(previous_hydration_fragment);
if (was_hydrating) {
set_hydrate_nodes(previous_hydrate_nodes);
}
}
}

@ -1,5 +1,3 @@
// Handle hydration
import { schedule_task } from './task.js';
import { empty } from './operations.js';
@ -9,66 +7,79 @@ import { empty } from './operations.js';
*/
export let hydrating = false;
/** @param {boolean} value */
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('../types.js').TemplateNode[]}
* @type {import('#client').TemplateNode[]}
*/
export let current_hydration_fragment = /** @type {any} */ (null);
export let hydrate_nodes = /** @type {any} */ (null);
/**
* @param {null | import('../types.js').TemplateNode[]} fragment
* @param {null | import('#client').TemplateNode[]} nodes
* @returns {void}
*/
export function set_current_hydration_fragment(fragment) {
hydrating = fragment !== null;
current_hydration_fragment = /** @type {import('../types.js').TemplateNode[]} */ (fragment);
export function set_hydrate_nodes(nodes) {
hydrate_nodes = /** @type {import('#client').TemplateNode[]} */ (nodes);
}
/**
* @param {Node | null} first
* @param {boolean} [insert_text] Whether to insert an empty text node if `nodes` is empty
*/
export function update_hydrate_nodes(first, insert_text) {
const nodes = get_hydrate_nodes(first, insert_text);
set_hydrate_nodes(nodes);
return nodes;
}
/**
* Returns all nodes between the first `<!--ssr:...-->` comment tag pair encountered.
* @param {Node | null} node
* @param {boolean} [insert_text] Whether to insert an empty text node if the fragment is empty
* @returns {import('../types.js').TemplateNode[] | null}
* @param {boolean} [insert_text] Whether to insert an empty text node if `nodes` is empty
* @returns {import('#client').TemplateNode[] | null}
*/
export function get_hydration_fragment(node, insert_text = false) {
/** @type {import('../types.js').TemplateNode[]} */
const fragment = [];
function get_hydrate_nodes(node, insert_text = false) {
/** @type {import('#client').TemplateNode[]} */
var nodes = [];
/** @type {null | Node} */
let current_node = node;
var current_node = /** @type {null | import('#client').TemplateNode} */ (node);
/** @type {null | string} */
let target_depth = null;
var target_depth = null;
while (current_node !== null) {
const node_type = current_node.nodeType;
const next_sibling = current_node.nextSibling;
if (node_type === 8) {
const data = /** @type {Comment} */ (current_node).data;
if (current_node.nodeType === 8) {
var data = /** @type {Comment} */ (current_node).data;
if (data.startsWith('ssr:')) {
const depth = data.slice(4);
var depth = data.slice(4);
if (target_depth === null) {
target_depth = depth;
} else if (depth === target_depth) {
if (insert_text && fragment.length === 0) {
const text = empty();
fragment.push(text);
/** @type {Node} */ (current_node.parentNode).insertBefore(text, current_node);
if (insert_text && nodes.length === 0) {
var text = empty();
nodes.push(text);
current_node.before(text);
}
return fragment;
return nodes;
} else {
fragment.push(/** @type {Text | Comment | Element} */ (current_node));
nodes.push(current_node);
}
current_node = next_sibling;
continue;
}
} else if (target_depth !== null) {
nodes.push(current_node);
}
if (target_depth !== null) {
fragment.push(/** @type {Text | Comment | Element} */ (current_node));
}
current_node = next_sibling;
current_node = /** @type {null | import('#client').TemplateNode} */ (current_node.nextSibling);
}
return null;
}
@ -81,18 +92,39 @@ export function hydrate_block_anchor(node) {
if (node.nodeType === 8) {
// @ts-ignore
let fragment = node.$$fragment;
if (fragment === undefined) {
fragment = get_hydration_fragment(node);
let nodes = node.$$fragment;
if (nodes === undefined) {
nodes = get_hydrate_nodes(node);
} else {
schedule_task(() => {
// @ts-expect-error clean up memory
node.$$fragment = undefined;
});
}
set_current_hydration_fragment(fragment);
set_hydrate_nodes(nodes);
} else {
const first_child = /** @type {Element | null} */ (node.firstChild);
set_current_hydration_fragment(first_child === null ? [] : [first_child]);
set_hydrate_nodes(first_child === null ? [] : [first_child]);
}
}
/**
* Expects to only be called in hydration mode
* @param {Node} node
* @returns {Node}
*/
export function capture_fragment_from_node(node) {
if (
node.nodeType === 8 &&
/** @type {Comment} */ (node).data.startsWith('ssr:') &&
hydrate_nodes[hydrate_nodes.length - 1] !== node
) {
const nodes = /** @type {Node[]} */ (get_hydrate_nodes(node));
const last_child = nodes[nodes.length - 1] || node;
const target = /** @type {Node} */ (last_child.nextSibling);
// @ts-ignore
target.$$fragment = nodes;
return target;
}
return node;
}

@ -1,4 +1,4 @@
import { current_hydration_fragment, get_hydration_fragment, hydrating } from './hydration.js';
import { capture_fragment_from_node, hydrate_nodes, hydrating } from './hydration.js';
import { get_descriptor } from '../utils.js';
// We cache the Node and Element prototype methods, so that we can avoid doing
@ -123,17 +123,14 @@ export function empty() {
/*#__NO_SIDE_EFFECTS__*/
export function child(node) {
const child = first_child_get.call(node);
if (hydrating) {
// Child can be null if we have an element with a single child, like `<p>{text}</p>`, where `text` is empty
if (child === null) {
const text = empty();
node.appendChild(text);
return text;
} else {
return capture_fragment_from_node(child);
}
if (!hydrating) return child;
// Child can be null if we have an element with a single child, like `<p>{text}</p>`, where `text` is empty
if (child === null) {
return node.appendChild(empty());
}
return child;
return capture_fragment_from_node(child);
}
/**
@ -144,28 +141,26 @@ export function child(node) {
*/
/*#__NO_SIDE_EFFECTS__*/
export function child_frag(node, is_text) {
if (hydrating) {
const first_node = /** @type {Node[]} */ (node)[0];
if (!hydrating) {
return first_child_get.call(/** @type {Node} */ (node));
}
// if an {expression} is empty during SSR, there might be no
// text node to hydrate — we must therefore create one
if (is_text && first_node?.nodeType !== 3) {
const text = empty();
current_hydration_fragment.unshift(text);
if (first_node) {
/** @type {DocumentFragment} */ (first_node.parentNode).insertBefore(text, first_node);
}
return text;
}
const first_node = /** @type {import('#client').TemplateNode[]} */ (node)[0];
if (first_node !== null) {
return capture_fragment_from_node(first_node);
}
// if an {expression} is empty during SSR, there might be no
// text node to hydrate — we must therefore create one
if (is_text && first_node?.nodeType !== 3) {
const text = empty();
hydrate_nodes.unshift(text);
first_node?.before(text);
return text;
}
return first_node;
if (first_node !== null) {
return capture_fragment_from_node(first_node);
}
return first_child_get.call(/** @type {Node} */ (node));
return first_node;
}
/**
@ -177,19 +172,18 @@ export function child_frag(node, is_text) {
/*#__NO_SIDE_EFFECTS__*/
export function sibling(node, is_text = false) {
const next_sibling = next_sibling_get.call(node);
if (hydrating) {
// 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) {
const text = empty();
if (next_sibling) {
const index = current_hydration_fragment.indexOf(
/** @type {Text | Comment | Element} */ (next_sibling)
);
current_hydration_fragment.splice(index, 0, text);
/** @type {DocumentFragment} */ (next_sibling.parentNode).insertBefore(text, next_sibling);
const index = hydrate_nodes.indexOf(/** @type {Text | Comment | Element} */ (next_sibling));
hydrate_nodes.splice(index, 0, text);
next_sibling.before(text);
} else {
current_hydration_fragment.push(text);
hydrate_nodes.push(text);
}
return text;
@ -199,6 +193,7 @@ export function sibling(node, is_text = false) {
return capture_fragment_from_node(next_sibling);
}
}
return next_sibling;
}
@ -226,24 +221,3 @@ export function clear_text_content(node) {
export function create_element(name) {
return document.createElement(name);
}
/**
* Expects to only be called in hydration mode
* @param {Node} node
* @returns {Node}
*/
function capture_fragment_from_node(node) {
if (
node.nodeType === 8 &&
/** @type {Comment} */ (node).data.startsWith('ssr:') &&
current_hydration_fragment[current_hydration_fragment.length - 1] !== node
) {
const fragment = /** @type {Array<Element | Text | Comment>} */ (get_hydration_fragment(node));
const last_child = fragment[fragment.length - 1] || node;
const target = /** @type {Node} */ (last_child.nextSibling);
// @ts-ignore
target.$$fragment = fragment;
return target;
}
return node;
}

@ -1,5 +1,5 @@
import { append_child } from './operations.js';
import { current_hydration_fragment, hydrate_block_anchor, hydrating } from './hydration.js';
import { hydrate_nodes, hydrate_block_anchor, hydrating } from './hydration.js';
import { is_array } from '../utils.js';
/** @param {string} html */
@ -76,7 +76,7 @@ export function remove(current) {
export function reconcile_html(target, value, svg) {
hydrate_block_anchor(target);
if (hydrating) {
return current_hydration_fragment;
return hydrate_nodes;
}
var html = value + '';
// Even if html is the empty string we need to continue to insert something or

@ -1,4 +1,4 @@
import { current_hydration_fragment, hydrate_block_anchor, hydrating } from './hydration.js';
import { hydrate_nodes, hydrate_block_anchor, hydrating } from './hydration.js';
import { child, clone_node, empty } from './operations.js';
import {
create_fragment_from_html,
@ -94,9 +94,9 @@ function open_template(is_fragment, use_clone_node, anchor, template_element_fn)
}
// In ssr+hydration optimization mode, we might remove the template_element,
// so we need to is_fragment flag to properly handle hydrated content accordingly.
const fragment = current_hydration_fragment;
if (fragment !== null) {
return is_fragment ? fragment : /** @type {Element} */ (fragment[0]);
const nodes = hydrate_nodes;
if (nodes !== null) {
return is_fragment ? nodes : /** @type {Element} */ (nodes[0]);
}
}
return use_clone_node

@ -5,11 +5,12 @@ import { remove } from './dom/reconciler.js';
import { flush_sync, push, pop, current_component_context } from './runtime.js';
import { render_effect, destroy_effect } from './reactivity/effects.js';
import {
current_hydration_fragment,
get_hydration_fragment,
hydrate_nodes,
hydrate_block_anchor,
hydrating,
set_current_hydration_fragment
set_hydrate_nodes,
set_hydrating,
update_hydrate_nodes
} from './dom/hydration.js';
import { array_from } from './utils.js';
import { handle_event_propagation } from './dom/elements/events.js';
@ -113,7 +114,6 @@ export function createRoot() {
* @returns {Exports}
*/
export function mount(component, options) {
init_operations();
const anchor = empty();
options.target.appendChild(anchor);
// Don't flush previous effects to ensure order of outer effects stays consistent
@ -138,18 +138,20 @@ export function mount(component, options) {
* @returns {Exports}
*/
export function hydrate(component, options) {
init_operations();
const container = options.target;
const first_child = /** @type {ChildNode} */ (container.firstChild);
const previous_hydrate_nodes = hydrate_nodes;
// Call with insert_text == true to prevent empty {expressions} resulting in an empty
// fragment array, resulting in a hydration error down the line
const hydration_fragment = get_hydration_fragment(first_child, true);
const previous_hydration_fragment = current_hydration_fragment;
set_current_hydration_fragment(hydration_fragment);
// `nodes` array, resulting in a hydration error down the line
// TODO is both this and the `container.appendChild(anchor)` below necessary?
const nodes = update_hydrate_nodes(first_child, true);
set_hydrating(true);
/** @type {null | Text} */
let anchor = null;
if (hydration_fragment === null) {
if (nodes === null) {
anchor = empty();
container.appendChild(anchor);
}
@ -162,12 +164,12 @@ export function hydrate(component, options) {
const instance = _mount(component, { ...options, anchor });
// flush_sync will run this callback and then synchronously run any pending effects,
// which don't belong to the hydration phase anymore - therefore reset it here
set_current_hydration_fragment(null);
set_hydrating(false);
finished_hydrating = true;
return instance;
}, false);
} catch (error) {
if (!finished_hydrating && options.recover !== false && hydration_fragment !== null) {
if (!finished_hydrating && options.recover !== false && nodes !== null) {
// eslint-disable-next-line no-console
console.error(
'ERR_SVELTE_HYDRATION_MISMATCH' +
@ -176,16 +178,17 @@ export function hydrate(component, options) {
: ''),
error
);
remove(hydration_fragment);
remove(nodes);
first_child.remove();
hydration_fragment[hydration_fragment.length - 1]?.nextSibling?.remove();
set_current_hydration_fragment(null);
nodes[nodes.length - 1]?.nextSibling?.remove();
set_hydrating(false);
return mount(component, options);
} else {
throw error;
}
} finally {
set_current_hydration_fragment(previous_hydration_fragment);
set_hydrating(!!previous_hydrate_nodes);
set_hydrate_nodes(previous_hydrate_nodes);
}
}
@ -206,6 +209,8 @@ export function hydrate(component, options) {
* @returns {Exports}
*/
function _mount(Component, options) {
init_operations();
const registered_events = new Set();
const container = options.target;

Loading…
Cancel
Save