setInsertionState,
} from '../insertionState'
import {
- _child,
disableHydrationNodeLookup,
enableHydrationNodeLookup,
next,
} from './node'
-import { isDynamicAnchor, isVaporFragmentEndAnchor } from '@vue/shared'
+import { isVaporAnchors, isVaporFragmentAnchor } from '@vue/shared'
export let isHydrating = false
export let currentHydrationNode: Node | null = null
// optimize anchor cache lookup
;(Comment.prototype as any).$fs = undefined
- ;(Node.prototype as any).$nc = undefined
isOptimized = true
}
enableHydrationNodeLookup()
return node
}
+const hydrationPositionMap = new WeakMap<ParentNode, Node>()
+
function locateHydrationNodeImpl(hasFragmentAnchor?: boolean) {
let node: Node | null
// prepend / firstChild
if (insertionAnchor === 0) {
- node = _child(insertionParent!)
+ node = insertionParent!.firstChild
} else if (insertionAnchor) {
- // for dynamic children, use insertionAnchor as the node
+ // `insertionAnchor` is a Node, it is the DOM node to hydrate
+ // Template: `...<span/><!----><span/>...`// `insertionAnchor` is the placeholder
+ // SSR Output: `...<span/>Content<span/>...`// `insertionAnchor` is the actual node
node = insertionAnchor
} else {
node = insertionParent
- ? insertionParent.$nc || insertionParent.lastChild
+ ? hydrationPositionMap.get(insertionParent) || insertionParent.lastChild
: currentHydrationNode
- // if the last child is a vapor fragment end anchor, find the previous one
- if (hasFragmentAnchor && node && isVaporFragmentEndAnchor(node)) {
+ // if node is a vapor fragment anchor, find the previous one
+ if (hasFragmentAnchor && node && isVaporFragmentAnchor(node)) {
node = node.previousSibling
if (__DEV__ && !node) {
- // TODO warning, should not happen
+ // this should not happen
+ throw new Error(`vapor fragment anchor previous node was not found.`)
}
}
}
if (insertionParent && node) {
- insertionParent.$nc = node!.previousSibling
+ const prev = node.previousSibling
+ if (prev) hydrationPositionMap.set(insertionParent, prev)
}
}
currentHydrationNode = node
}
-export function isEmptyText(node: Node): node is Text {
- return node.nodeType === 3 && !(node as Text).data.trim()
-}
-
export function locateEndAnchor(
node: Node | null,
open = '[',
export function isNonHydrationNode(node: Node): boolean {
return (
- // empty text nodes
- isEmptyText(node) ||
- // dynamic node anchors (<!--[[-->, <!--]]-->)
- isDynamicAnchor(node) ||
- // fragment end anchor (`<!--]-->`)
+ // empty text node
+ isEmptyTextNode(node) ||
+ // vdom fragment end anchor (`<!--]-->`)
isComment(node, ']') ||
- // vapor fragment end anchors
- isVaporFragmentEndAnchor(node)
+ // vapor mode specific anchors
+ isVaporAnchors(node)
)
}
-export function findVaporFragmentAnchor(
+export function locateVaporFragmentAnchor(
node: Node,
anchorLabel: string,
-): Comment | null {
+): Comment | undefined {
let n = node.nextSibling
while (n) {
if (isComment(n, anchorLabel)) return n
n = n.nextSibling
}
+}
- return null
+export function isEmptyTextNode(node: Node): node is Text {
+ return node.nodeType === 3 && !(node as Text).data.trim()
}
import {
DYNAMIC_END_ANCHOR_LABEL,
DYNAMIC_START_ANCHOR_LABEL,
- isVaporFragmentEndAnchor,
+ isVaporAnchors,
} from '@vue/shared'
/*! #__NO_SIDE_EFFECTS__ */
return node.firstChild!
}
+/**
+ * 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) // 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)` 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)` is used during hydration. It skips the SSR fragment anchors
+ * (`<!--[-->...<!--]-->`) and any other non-content nodes to find the
+ * "Actual Text Node", correctly matching the client's expectation for `n1`.
+ */
/*! #__NO_SIDE_EFFECTS__ */
export function __child(node: ParentNode): Node {
- /**
- * During hydration, the first child of a node not be the expected
- * if the first child is slot
- *
- * for template code: `div><slot />{{ data }}</div>`
- * - slot: 'slot',
- * - data: 'hi',
- *
- * client side:
- * const n2 = _template("<div> </div>")()
- * const n1 = _child(n2) -> the text node
- * _setInsertionState(n2, 0) -> slot fragment
- *
- * during hydration:
- * const n2 = <div><!--[-->slot<!--]--><!--slot-->Hi</div> // server output
- * const n1 = _child(n2) -> should be `Hi` instead of the slot fragment
- * _setInsertionState(n2, 0) -> slot fragment
- */
let n = node.firstChild!
if (isComment(n, '[')) {
n = locateEndAnchor(n)!.nextSibling!
}
- while (n && isVaporFragmentEndAnchor(n)) {
+ while (n && isVaporAnchors(n)) {
n = n.nextSibling!
}
return n
return node.childNodes[i]
}
+/**
+ * Hydration-specific version of `nthChild`.
+ */
/*! #__NO_SIDE_EFFECTS__ */
export function __nthChild(node: Node, i: number): Node {
let n = node.firstChild!
for (let start = 0; start < i; start++) {
- n = next(n) as ChildNode
+ n = __next(n) as ChildNode
}
return n
}
return node.nextSibling!
}
+/**
+ * Hydration-specific version of `next`.
+ *
+ * SSR comment anchors (fragments `<!--[-->...<!--]-->`, dynamic `<!--[[-->...<!--]]-->`)
+ * 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 dynamic component 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/dynamic blocks (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 {
// process dynamic node (<!--[[-->...<!--]]-->) as a single node
return n
}
-type ChildFn = (node: ParentNode) => Node
-type NextFn = (node: Node) => Node
-type NthChildFn = (node: Node, i: number) => Node
-
-interface DelegatedChildFunction extends ChildFn {
- impl: ChildFn
-}
-interface DelegatedNextFunction extends NextFn {
- impl: NextFn
-}
-interface DelegatedNthChildFunction extends NthChildFn {
- impl: NthChildFn
+type DelegatedFunction<T extends (...args: any[]) => any> = T & {
+ impl: T
}
/*! #__NO_SIDE_EFFECTS__ */
-export const child: DelegatedChildFunction = node => {
+export const child: DelegatedFunction<typeof _child> = node => {
return child.impl(node)
}
child.impl = _child
/*! #__NO_SIDE_EFFECTS__ */
-export const next: DelegatedNextFunction = node => {
+export const next: DelegatedFunction<typeof _next> = node => {
return next.impl(node)
}
next.impl = _next
/*! #__NO_SIDE_EFFECTS__ */
-export const nthChild: DelegatedNthChildFunction = (node, i) => {
+export const nthChild: DelegatedFunction<typeof _nthChild> = (node, i) => {
return nthChild.impl(node, i)
}
nthChild.impl = _nthChild
-// During hydration, there might be differences between the server-rendered (SSR)
-// HTML and the client-side template.
-// For example, a dynamic node `<!>` in the template might be rendered as a
-// `Fragment` (`<!--[-->...<!--]-->`) in the SSR output.
-// The content of the `Fragment` affects the lookup results of the `next` and
-// `nthChild` functions.
-// To ensure the hydration process correctly finds nodes, we need to treat the
-// `Fragment` as a single node.
-// Therefore, during hydration, we need to temporarily switch the implementations
-// of `next` and `nthChild`. After hydration is complete, their implementations
-// are restored to the original versions.
+/**
+ * Enables hydration-specific node lookup behavior.
+ *
+ * Temporarily switches the implementations of the exported
+ * `child`, `next`, and `nthChild` functions to their hydration-specific
+ * versions (`__child`, `__next`, `__nthChild`). This allows traversal
+ * logic to correctly handle SSR comment anchors during hydration.
+ */
export function enableHydrationNodeLookup(): void {
child.impl = __child
next.impl = __next