]> git.ipfire.org Git - thirdparty/vuejs/core.git/commitdiff
chore: tweaks
authordaiwei <daiwei521@126.com>
Wed, 30 Apr 2025 00:33:57 +0000 (08:33 +0800)
committerdaiwei <daiwei521@126.com>
Wed, 30 Apr 2025 06:26:24 +0000 (14:26 +0800)
packages/compiler-ssr/src/ssrCodegenTransform.ts
packages/runtime-core/src/hydration.ts
packages/runtime-core/src/renderer.ts
packages/runtime-vapor/src/apiCreateFor.ts
packages/runtime-vapor/src/block.ts
packages/runtime-vapor/src/dom/hydration.ts
packages/runtime-vapor/src/dom/node.ts
packages/runtime-vapor/src/insertionState.ts
packages/shared/src/domAnchors.ts

index 29f198370aa5587084cea67320273286050c3388..852e02820cc87cc12dd0b80897dddec27e235dd3 100644 (file)
@@ -410,7 +410,7 @@ function shouldProcessChildAsDynamic(
   if (dynamicNodeCount === 2) {
     return prevDynamicCount > 0
   }
-  // For three or more dynamic nodes, mark the intermediate node as dynamic
+  // For three or more dynamic nodes, mark the middle nodes as dynamic
   else if (dynamicNodeCount >= 3) {
     return prevDynamicCount > 0 && nextDynamicCount > 0
   }
index e1420d33536619b088e692ce7d18fa4319e868c8..8ed7b6af18953170d7a75cd9ebbbb8321984cc02 100644 (file)
@@ -25,14 +25,13 @@ import {
   getEscapedCssVarName,
   includeBooleanAttr,
   isBooleanAttr,
-  isDynamicAnchor,
   isKnownHtmlAttr,
   isKnownSvgAttr,
   isOn,
   isRenderableAttrValue,
   isReservedProp,
   isString,
-  isVaporFragmentEndAnchor,
+  isVaporAnchors,
   normalizeClass,
   normalizeStyle,
   stringifyStyle,
@@ -127,10 +126,8 @@ export function createHydrationFunctions(
 
   function nextSibling(node: Node) {
     let n = next(node)
-    // skip if:
-    // - dynamic anchors (`<!--[[-->`, `<!--][-->`)
-    // - vapor fragment end anchors (e.g. `<!--if-->`, `<!--for-->`)
-    if (n && (isDynamicAnchor(n) || isVaporFragmentEndAnchor(n))) {
+    // skip vapor mode specific anchors
+    if (n && isVaporAnchors(n)) {
       n = next(n)
     }
     return n
@@ -162,7 +159,8 @@ export function createHydrationFunctions(
     slotScopeIds: string[] | null,
     optimized = false,
   ): Node | null => {
-    if (isDynamicAnchor(node) || isVaporFragmentEndAnchor(node)) {
+    // skip vapor mode specific anchors
+    if (isVaporAnchors(node)) {
       node = nextSibling(node)!
     }
     optimized = optimized || !!vnode.dynamicChildren
index fade2a4b393e020143de9f71190bf8c0bbc61a27..bb895a480ad6176640c4822dee9e2177c3cc20f2 100644 (file)
@@ -107,7 +107,7 @@ export interface Renderer<HostElement = RendererElement> {
 
 export interface HydrationRenderer extends Renderer<Element | ShadowRoot> {
   hydrate: RootHydrateFunction
-  hydrateNode: ReturnType<typeof createHydrationFunctions>[1] | undefined
+  hydrateNode: ReturnType<typeof createHydrationFunctions>[1]
 }
 
 export type ElementNamespace = 'svg' | 'mathml' | undefined
index 06a2bf752ef4fe80e976538fb8169e3e87250000..7138d01a6af5e352c226ceb1c760d9ca79494175 100644 (file)
@@ -30,9 +30,9 @@ import { renderEffect } from './renderEffect'
 import { VaporVForFlags } from '../../shared/src/vaporFlags'
 import {
   currentHydrationNode,
-  findVaporFragmentAnchor,
   isHydrating,
   locateHydrationNode,
+  locateVaporFragmentAnchor,
 } from './dom/hydration'
 import {
   insertionAnchor,
@@ -97,13 +97,13 @@ export const createFor = (
   let parent: ParentNode | undefined | null
   let parentAnchor: Node
   if (isHydrating) {
-    parentAnchor = findVaporFragmentAnchor(
+    parentAnchor = locateVaporFragmentAnchor(
       currentHydrationNode!,
       FOR_ANCHOR_LABEL,
     )!
     if (__DEV__ && !parentAnchor) {
-      // TODO warn, should not happen
-      warn(`createFor anchor not found...`)
+      // this should not happen
+      throw new Error(`v-for fragment anchor node was not found.`)
     }
   } else {
     parentAnchor = __DEV__ ? createComment('for') : createTextNode()
index 50e2e4b26fd5100a2c4b756cf4bb1f87158c85f5..d6be89efc39fb0ac23b7d41f83aa62cfd25dd07f 100644 (file)
@@ -9,12 +9,11 @@ import { createComment, createTextNode } from './dom/node'
 import { EffectScope, pauseTracking, resetTracking } from '@vue/reactivity'
 import {
   currentHydrationNode,
-  findVaporFragmentAnchor,
   isComment,
   isHydrating,
   locateHydrationNode,
+  locateVaporFragmentAnchor,
 } from './dom/hydration'
-import { warn } from '@vue/runtime-dom'
 
 export type Block =
   | Node
@@ -89,17 +88,17 @@ export class DynamicFragment extends VaporFragment {
   }
 
   hydrate(label: string): void {
-    // for `v-if="false"` the node will be an empty comment node use it as the anchor.
+    // for `v-if="false"` the node will be an empty comment, use it as the anchor.
     // otherwise, find next sibling vapor fragment anchor
     if (isComment(currentHydrationNode!, '')) {
       this.anchor = currentHydrationNode
     } else {
-      const anchor = findVaporFragmentAnchor(currentHydrationNode!, label)!
+      const anchor = locateVaporFragmentAnchor(currentHydrationNode!, label)!
       if (anchor) {
         this.anchor = anchor
       } else if (__DEV__) {
-        // TODO warning, should not happen
-        warn(`DynamicFragment anchor not found...`)
+        // this should not happen
+        throw new Error(`${label} fragment anchor node was not found.`)
       }
     }
   }
index d82aa33f984ea5758e0aa324454bac0838408c66..e3f666b5b2652708168a97ecda58b8efeb37a5b7 100644 (file)
@@ -6,12 +6,11 @@ import {
   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
@@ -33,7 +32,6 @@ function performHydration<T>(
 
     // optimize anchor cache lookup
     ;(Comment.prototype as any).$fs = undefined
-    ;(Node.prototype as any).$nc = undefined
     isOptimized = true
   }
   enableHydrationNodeLookup()
@@ -101,24 +99,29 @@ function adoptTemplateImpl(node: Node, template: string): Node | null {
   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.`)
       }
     }
 
@@ -153,7 +156,8 @@ function locateHydrationNodeImpl(hasFragmentAnchor?: boolean) {
     }
 
     if (insertionParent && node) {
-      insertionParent.$nc = node!.previousSibling
+      const prev = node.previousSibling
+      if (prev) hydrationPositionMap.set(insertionParent, prev)
     }
   }
 
@@ -166,10 +170,6 @@ function locateHydrationNodeImpl(hasFragmentAnchor?: boolean) {
   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 = '[',
@@ -194,26 +194,26 @@ export function locateEndAnchor(
 
 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()
 }
index 2384697ed0341c52911abf119a2f99e01d9e2030..3f38c477a01158ea5b56bd0f5cd1457a9094cda7 100644 (file)
@@ -2,7 +2,7 @@ import { isComment, isNonHydrationNode, locateEndAnchor } from './hydration'
 import {
   DYNAMIC_END_ANCHOR_LABEL,
   DYNAMIC_START_ANCHOR_LABEL,
-  isVaporFragmentEndAnchor,
+  isVaporAnchors,
 } from '@vue/shared'
 
 /*! #__NO_SIDE_EFFECTS__ */
@@ -25,33 +25,43 @@ export function _child(node: ParentNode): Node {
   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
@@ -62,11 +72,14 @@ export function _nthChild(node: Node, i: number): Node {
   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
 }
@@ -76,6 +89,46 @@ function _next(node: Node): Node {
   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
@@ -99,49 +152,36 @@ export function __next(node: Node): 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
index b33c820e0132c1cb5ab17a7c40715129926bff61..c8c7ffbcd1de3b1000cbc9fa62f50ee9c8c3a0b3 100644 (file)
@@ -1,9 +1,4 @@
-export let insertionParent:
-  | (ParentNode & {
-      // the next child node to be hydrated
-      $nc?: Node | null
-    })
-  | undefined
+export let insertionParent: ParentNode | undefined
 export let insertionAnchor: Node | 0 | undefined
 
 /**
index e93bc01889dab776e973e24447332d640ab0be8a..f807ee169db17db8f0bec8216ea41ffb10b39e24 100644 (file)
@@ -15,7 +15,7 @@ export function isDynamicAnchor(node: Node): node is Comment {
   )
 }
 
-export function isVaporFragmentEndAnchor(node: Node): node is Comment {
+export function isVaporFragmentAnchor(node: Node): node is Comment {
   if (node.nodeType !== 8) return false
 
   const data = (node as Comment).data
@@ -26,3 +26,7 @@ export function isVaporFragmentEndAnchor(node: Node): node is Comment {
     data === DYNAMIC_COMPONENT_ANCHOR_LABEL
   )
 }
+
+export function isVaporAnchors(node: Node): node is Comment {
+  return isDynamicAnchor(node) || isVaporFragmentAnchor(node)
+}