BLOCK_APPEND_ANCHOR_LABEL,
BLOCK_INSERTION_ANCHOR_LABEL,
BLOCK_PREPEND_ANCHOR_LABEL,
- isVaporAnchor,
} from '@vue/shared'
const isHydratingStack = [] as boolean[]
-
export let isHydrating = false
export let currentHydrationNode: Node | null = null
-export function setCurrentHydrationNode(node: Node | null): void {
- currentHydrationNode = node
-}
-
-function findParentSibling(n: Node): Node | null {
- if (!n.parentNode) return null
- return n.parentNode.nextSibling || findParentSibling(n.parentNode)
-}
-
-export function advanceHydrationNode(node: Node & { $ps?: Node | null }): void {
- // if no next sibling, find the next node in the parent chain
- const ret =
- node.nextSibling || node.$ps || (node.$ps = findParentSibling(node))
- if (ret) setCurrentHydrationNode(ret)
-}
-
let isOptimized = false
function performHydration<T>(
export const isComment = (node: Node, data: string): node is Anchor =>
node.nodeType === 8 && (node as Comment).data === data
+export function setCurrentHydrationNode(node: Node | null): void {
+ currentHydrationNode = node
+}
+
+function findParentSibling(n: Node): Node | null {
+ if (!n.parentNode) return null
+ return n.parentNode.nextSibling || findParentSibling(n.parentNode)
+}
+
+export function advanceHydrationNode(node: Node & { $ps?: Node | null }): void {
+ // if no next sibling, find the next node in the parent chain
+ const ret =
+ node.nextSibling || node.$ps || (node.$ps = findParentSibling(node))
+ if (ret) setCurrentHydrationNode(ret)
+}
+
/**
* Locate the first non-fragment-comment node and locate the next node
* while handling potential fragments.
return null
}
-export function isNonHydrationNode(node: Node): boolean {
- return (
- // empty text node
- isEmptyTextNode(node) ||
- // vdom fragment end anchor (`<!--]-->`)
- isComment(node, ']') ||
- // vapor mode specific anchors
- isVaporAnchor(node)
- )
-}
-
export function locateVaporFragmentAnchor(
node: Node,
anchorLabel: string,
return null
}
-export function isEmptyTextNode(node: Node): node is Text {
- return node.nodeType === 3 && !(node as Text).data.trim()
-}
-
function locateHydrationNodeByAnchor(
node: Node,
anchorLabel: string,
-import { isComment, isNonHydrationNode, locateEndAnchor } from './hydration'
+import { isComment, locateEndAnchor } from './hydration'
import {
BLOCK_INSERTION_ANCHOR_LABEL,
BLOCK_PREPEND_ANCHOR_LABEL,
- isVaporAnchor,
+ isInsertionAnchor,
} from '@vue/shared'
/*! #__NO_SIDE_EFFECTS__ */
/**
* Hydration-specific version of `child`.
- *
- * This function skips leading fragment anchors to find the first node relevant
- * for hydration matching against the client-side template structure.
- *
- * Problem:
- * Template: `<div><slot />{{ msg }}</div>`
- *
- * Client Compiled Code (Simplified):
- * const n2 = t0() // n2 = `<div> </div>`
- * const n1 = _child(n2, 1) // n1 = text node
- * // ... slot creation ...
- * _renderEffect(() => _setText(n1, _ctx.msg))
- *
- * SSR Output: `<div><!--[-->slot content<!--]-->Actual Text Node</div>`
- *
- * Hydration Mismatch:
- * - During hydration, `n2` refers to the SSR `<div>`.
- * - `_child(n2, 1)` would return `<!--[-->`.
- * - The client code expects `n1` to be the text node, but gets the comment.
- * The subsequent `_setText(n1, ...)` would fail or target the wrong node.
- *
- * Solution (`__child`):
- * - `__child(n2, offset)` is used during hydration. It skips the block children
- * to find the "Actual Text Node", correctly matching the client's expectation
- * for `n1`.
*/
/*! #__NO_SIDE_EFFECTS__ */
export function __child(node: ParentNode, offset?: number): Node {
let n = node.firstChild!
// when offset is -1, it means we need to get the text node of this element
- // since server-side rendering doesn't generate whitespace placeholder text nodes,
- // if firstChild is null, manually insert a text node and return it
+ // since SSR doesn't generate whitespace placeholder text nodes, if firstChild
+ // is null, manually insert a text node as the first child
if (offset === -1 && !n) {
node.textContent = ' '
return node.firstChild!
}
- while (n && (isComment(n, '[') || isVaporAnchor(n))) {
+ while (n && (isComment(n, '[') || isInsertionAnchor(n))) {
// skip block node
n = skipBlockNodes(n) as ChildNode
if (isComment(n, '[')) {
/**
* Hydration-specific version of `next`.
- *
- * SSR comment anchors (fragments `<!--[-->...<!--]-->`, block nodes `<!--[x-->...<!--x]-->`)
- * disrupt standard `node.nextSibling` traversal during hydration. `_next` might
- * return a comment node or an internal node of a fragment instead of skipping
- * the entire fragment block.
- *
- * Example:
- * Template: `<div>Node1<!>Node2</div>` (where <!> is a block node placeholder)
- *
- * Client Compiled Code (Simplified):
- * const n2 = t0() // n2 = `<div>Node1<!---->Node2</div>`
- * const n1 = _next(_child(n2)) // n1 = _next(Node1) returns `<!---->`
- * _setInsertionState(n2, n1) // insertion anchor is `<!---->`
- * const n0 = _createComponent(_ctx.Comp) // inserted before `<!---->`
- *
- * SSR Output: `<div>Node1<!--[-->Node3 Node4<!--]-->Node2</div>`
- *
- * Hydration Mismatch:
- * - During hydration, `n2` refers to the SSR `<div>`.
- * - `_child(n2)` returns `Node1`.
- * - `_next(Node1)` would return `<!--[-->`.
- * - The client logic expects `n1` to be the node *after* `Node1` in its structure
- * (the placeholder), but gets the fragment start anchor `<!--[-->` from SSR.
- * - Using `<!--[-->` as the insertion anchor for hydrating the component is incorrect.
- *
- * Solution (`__next`):
- * - During hydration, `next.impl` is `__next`.
- * - `n1 = __next(Node1)` is called.
- * - `__next` recognizes that the immediate sibling `<!--[-->` is a fragment start anchor.
- * - It skips the entire fragment block (`<!--[-->Node3 Node4<!--]-->`).
- * - It returns the node immediately *after* the fragment's end anchor, which is `Node2`.
- * - This correctly identifies the logical "next sibling" anchor (`Node2`) in the SSR structure,
- * allowing the component to be hydrated correctly relative to `Node1` and `Node2`.
- *
- * This function ensures traversal correctly skips over non-hydration nodes and
- * treats entire fragment/block nodes (when starting *from* their beginning anchor)
- * as single logical units to find the next actual sibling node for hydration matching.
*/
/*! #__NO_SIDE_EFFECTS__ */
export function __next(node: Node): Node {
if (isComment(node, '[')) {
node = locateEndAnchor(node)!
}
-
node = skipBlockNodes(node)
-
- let n = node.nextSibling!
- while (n && isNonHydrationNode(n)) {
- n = n.nextSibling!
- }
- return n
+ return node.nextSibling!
}
type DelegatedFunction<T extends (...args: any[]) => any> = T & {
export function isInsertionAnchor(node: Node): node is Comment {
if (node.nodeType !== 8) return false
-
const data = (node as Comment).data
return (
data === `[${BLOCK_INSERTION_ANCHOR_LABEL}` ||
- data === `${BLOCK_INSERTION_ANCHOR_LABEL}]` ||
data === `[${BLOCK_APPEND_ANCHOR_LABEL}` ||
- data === `${BLOCK_APPEND_ANCHOR_LABEL}]` ||
- data === `[${BLOCK_PREPEND_ANCHOR_LABEL}` ||
- data === `${BLOCK_PREPEND_ANCHOR_LABEL}]`
- )
-}
-
-export function isVaporFragmentAnchor(node: Node): node is Comment {
- if (node.nodeType !== 8) return false
-
- const data = (node as Comment).data
- return (
- data === IF_ANCHOR_LABEL ||
- data === FOR_ANCHOR_LABEL ||
- data === SLOT_ANCHOR_LABEL ||
- data === DYNAMIC_COMPONENT_ANCHOR_LABEL
+ data === `[${BLOCK_PREPEND_ANCHOR_LABEL}`
)
}
-
-export function isVaporAnchor(node: Node): node is Comment {
- return isVaporFragmentAnchor(node) || isInsertionAnchor(node)
-}