diff --git a/packages/runtime-core/src/hydration.ts b/packages/runtime-core/src/hydration.ts index 13f900a94..94291d409 100644 --- a/packages/runtime-core/src/hydration.ts +++ b/packages/runtime-core/src/hydration.ts @@ -84,6 +84,10 @@ const getContainerType = ( return undefined } +export function isDynamicAnchor(node: Node): boolean { + return isComment(node) && (node.data === '[[' || node.data === ']]') +} + export const isComment = (node: Node): node is Comment => node.nodeType === DOMNodeTypes.COMMENT @@ -119,10 +123,6 @@ export function createHydrationFunctions( }, } = rendererInternals - function isDynamicAnchor(node: Node): boolean { - return isComment(node) && (node.data === '[[' || node.data === ']]') - } - function nextSibling(node: Node) { let n = next(node) // skip dynamic child anchor diff --git a/packages/runtime-core/src/index.ts b/packages/runtime-core/src/index.ts index e309554f2..4c31ff510 100644 --- a/packages/runtime-core/src/index.ts +++ b/packages/runtime-core/src/index.ts @@ -557,3 +557,7 @@ export { startMeasure, endMeasure } from './profiling' * @internal */ export { initFeatureFlags } from './featureFlags' +/** + * @internal + */ +export { isDynamicAnchor } from './hydration' diff --git a/packages/runtime-vapor/src/dom/hydration.ts b/packages/runtime-vapor/src/dom/hydration.ts index 37568b136..55dd98532 100644 --- a/packages/runtime-vapor/src/dom/hydration.ts +++ b/packages/runtime-vapor/src/dom/hydration.ts @@ -1,12 +1,16 @@ import { warn } from '@vue/runtime-dom' import { - type Anchor, insertionAnchor, insertionParent, resetInsertionState, setInsertionState, } from '../insertionState' -import { child, next } from './node' +import { + child, + disableHydrationNodeLookup, + enableHydrationNodeLookup, + next, +} from './node' export let isHydrating = false export let currentHydrationNode: Node | null = null @@ -25,18 +29,26 @@ export function withHydration(container: ParentNode, fn: () => void): void { ;(Comment.prototype as any).$fs = undefined isOptimized = true } + enableHydrationNodeLookup() isHydrating = true setInsertionState(container, 0) const res = fn() resetInsertionState() currentHydrationNode = null isHydrating = false + disableHydrationNodeLookup() return res } export let adoptTemplate: (node: Node, template: string) => Node | null export let locateHydrationNode: () => void +type Anchor = Comment & { + // cached matching fragment start to avoid repeated traversal + // on nested fragments + $fs?: Anchor +} + export const isComment = (node: Node, data: string): node is Anchor => node.nodeType === 8 && (node as Comment).data === data @@ -120,10 +132,6 @@ function locateHydrationNodeImpl() { currentHydrationNode = node } -export function isDynamicAnchor(node: Node): node is Comment { - return isComment(node, '[[') || isComment(node, ']]') -} - export function isEmptyText(node: Node): node is Text { return node.nodeType === 3 && !(node as Text).data.trim() } diff --git a/packages/runtime-vapor/src/dom/node.ts b/packages/runtime-vapor/src/dom/node.ts index 3a1e283b1..91fc2714f 100644 --- a/packages/runtime-vapor/src/dom/node.ts +++ b/packages/runtime-vapor/src/dom/node.ts @@ -1,10 +1,5 @@ -import { - isComment, - isDynamicAnchor, - isEmptyText, - isHydrating, - locateEndAnchor, -} from './hydration' +import { isDynamicAnchor } from '@vue/runtime-dom' +import { isComment, isEmptyText, locateEndAnchor } from './hydration' /*! #__NO_SIDE_EFFECTS__ */ export function createTextNode(value = ''): Text { @@ -27,9 +22,12 @@ export function child(node: ParentNode): Node { } /*! #__NO_SIDE_EFFECTS__ */ -export function nthChild(node: Node, i: number): Node { - if (!isHydrating) return node.childNodes[i] +export function _nthChild(node: Node, i: number): Node { + return node.childNodes[i] +} +/*! #__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 @@ -38,9 +36,12 @@ export function nthChild(node: Node, i: number): Node { } /*! #__NO_SIDE_EFFECTS__ */ -export function next(node: Node): Node { - if (!isHydrating) return node.nextSibling! +function _next(node: Node): Node { + return node.nextSibling! +} +/*! #__NO_SIDE_EFFECTS__ */ +function __next(node: Node): Node { // process fragment as a single node if (node && isComment(node, '[')) { node = locateEndAnchor(node)! @@ -53,3 +54,46 @@ export function next(node: Node): Node { } return n } + +type NextFn = (node: Node) => Node +type NthChildFn = (node: Node, i: number) => Node + +interface DelegatedNextFunction extends NextFn { + impl: NextFn +} +interface DelegatedNthChildFunction extends NthChildFn { + impl: NthChildFn +} + +/*! #__NO_SIDE_EFFECTS__ */ +export const next: DelegatedNextFunction = node => { + return next.impl(node) +} +next.impl = _next + +/*! #__NO_SIDE_EFFECTS__ */ +export const nthChild: DelegatedNthChildFunction = (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. +export function enableHydrationNodeLookup(): void { + next.impl = __next + nthChild.impl = __nthChild +} + +export function disableHydrationNodeLookup(): void { + next.impl = _next + nthChild.impl = _nthChild +} diff --git a/packages/runtime-vapor/src/insertionState.ts b/packages/runtime-vapor/src/insertionState.ts index 8280b65c2..c8c7ffbcd 100644 --- a/packages/runtime-vapor/src/insertionState.ts +++ b/packages/runtime-vapor/src/insertionState.ts @@ -1,10 +1,5 @@ -export let insertionParent: - | (ParentNode & { - // cached the last dynamic start anchor - $lds?: Anchor - }) - | undefined -export let insertionAnchor: Node | 0 | undefined | null +export let insertionParent: ParentNode | undefined +export let insertionAnchor: Node | 0 | undefined /** * This function is called before a block type that requires insertion @@ -19,13 +14,3 @@ export function setInsertionState(parent: ParentNode, anchor?: Node | 0): void { export function resetInsertionState(): void { insertionParent = insertionAnchor = undefined } - -export function setInsertionAnchor(anchor: Node | null): void { - insertionAnchor = anchor -} - -export type Anchor = Comment & { - // cached matching fragment start to avoid repeated traversal - // on nested fragments - $fs?: Anchor -}