]> git.ipfire.org Git - thirdparty/vuejs/core.git/commitdiff
refactor: reuse hydration state
authordaiwei <daiwei521@126.com>
Fri, 19 Sep 2025 07:50:03 +0000 (15:50 +0800)
committerdaiwei <daiwei521@126.com>
Fri, 19 Sep 2025 14:48:45 +0000 (22:48 +0800)
packages/runtime-vapor/src/component.ts
packages/runtime-vapor/src/dom/hydration.ts
packages/runtime-vapor/src/dom/node.ts
packages/runtime-vapor/src/dom/prop.ts
packages/runtime-vapor/src/dom/template.ts
packages/runtime-vapor/src/fragment.ts
packages/runtime-vapor/src/insertionState.ts

index 9d4c5737f68df8dca2a55d867f17896380757642..8d703220921952cc4616fbbe6a0a757f5b3a7594 100644 (file)
@@ -616,10 +616,8 @@ export function mountComponent(
     startMeasure(instance, `mount`)
   }
   if (instance.bm) invokeArrayFns(instance.bm)
-  if (!isHydrating) {
-    insert(instance.block, parent, anchor)
-    setComponentScopeId(instance)
-  }
+  insert(instance.block, parent, anchor)
+  if (!isHydrating) setComponentScopeId(instance)
   if (instance.m) queuePostFlushCb(() => invokeArrayFns(instance.m!))
   instance.isMounted = true
   if (__DEV__) {
index 9a4d923f74d6f08a29f662d9916bf75d51eedb5f..704494bad3c828c6c38805387f505796abef0137 100644 (file)
@@ -1,7 +1,7 @@
 import { warn } from '@vue/runtime-dom'
 import {
   type ChildItem,
-  getHydrationState,
+  incrementIndexOffset,
   insertionAnchor,
   insertionParent,
   resetInsertionState,
@@ -30,9 +30,15 @@ function performHydration<T>(
     // optimize anchor cache lookup
     ;(Comment.prototype as any).$fe = undefined
     ;(Node.prototype as any).$pns = undefined
-    ;(Node.prototype as any).$idx = undefined
     ;(Node.prototype as any).$uc = undefined
+    ;(Node.prototype as any).$idx = undefined
     ;(Node.prototype as any).$children = undefined
+    ;(Node.prototype as any).$idxMap = undefined
+    ;(Node.prototype as any).$prevDynamicCount = undefined
+    ;(Node.prototype as any).$anchorCount = undefined
+    ;(Node.prototype as any).$appendIndex = undefined
+    ;(Node.prototype as any).$indexOffset = undefined
+
     isOptimized = true
   }
   enableHydrationNodeLookup()
@@ -108,6 +114,7 @@ function adoptTemplateImpl(node: Node, template: string): Node | null {
         isComment(node.previousSibling!, '[')
       ) {
         node = node.parentNode!.insertBefore(createTextNode(' '), node)
+        incrementIndexOffset(node.parentNode!)
         break
       }
     }
@@ -136,13 +143,19 @@ function adoptTemplateImpl(node: Node, template: string): Node | null {
 
 function locateHydrationNodeImpl(): void {
   let node: Node | null
-  if (insertionAnchor !== undefined) {
-    const hydrationState = getHydrationState(insertionParent!)!
-    const { prevDynamicCount, logicalChildren, appendAnchor } = hydrationState
+  let idxMap: number[] | undefined
+  if (insertionAnchor !== undefined && (idxMap = insertionParent!.$idxMap)) {
+    const {
+      $prevDynamicCount: prevDynamicCount = 0,
+      $appendIndex: appendIndex,
+      $indexOffset: indexOffset = 0,
+      $anchorCount: anchorCount = 0,
+    } = insertionParent!
     // prepend
     if (insertionAnchor === 0) {
-      // use prevDynamicCount as index to locate the hydration node
-      node = logicalChildren[prevDynamicCount]
+      // use prevDynamicCount as logical index to locate the hydration node
+      const realIndex = idxMap![prevDynamicCount] + indexOffset
+      node = insertionParent!.childNodes[realIndex]
     }
     // insert
     else if (insertionAnchor instanceof Node) {
@@ -153,36 +166,42 @@ function locateHydrationNodeImpl(): void {
       // consecutive insert operations locate the correct hydration node.
       let { $idx, $uc: usedCount } = insertionAnchor as ChildItem
       if (usedCount !== undefined) {
-        node = logicalChildren[$idx + usedCount + 1]
+        const realIndex = idxMap![$idx + usedCount + 1] + indexOffset
+        node = insertionParent!.childNodes[realIndex]
         usedCount++
       } else {
         node = insertionAnchor
         // first use of this anchor: it doesn't consume the next child
         // so we track unique anchor appearances for later offset correction
-        hydrationState.uniqueAnchorCount++
+        insertionParent!.$anchorCount = anchorCount + 1
         usedCount = 0
       }
       ;(insertionAnchor as ChildItem).$uc = usedCount
     }
     // append
     else {
-      if (appendAnchor) {
-        node = logicalChildren[(appendAnchor as ChildItem).$idx + 1]
+      let realIndex: number
+      if (appendIndex !== null && appendIndex !== undefined) {
+        realIndex = idxMap![appendIndex + 1] + indexOffset
+        node = insertionParent!.childNodes[realIndex]
       } else {
-        node =
+        if (insertionAnchor === null) {
           // insertionAnchor is null, indicates no previous static nodes
           // use the first child as hydration node
-          insertionAnchor === null
-            ? logicalChildren[0]
-            : // insertionAnchor is a number > 0
-              // indicates how many static nodes precede the node to append
-              // use it as index to locate the hydration node
-              logicalChildren[prevDynamicCount + insertionAnchor]
+          realIndex = idxMap![0] + indexOffset
+          node = insertionParent!.childNodes[realIndex]
+        } else {
+          // insertionAnchor is a number > 0
+          // indicates how many static nodes precede the node to append
+          // use it as index to locate the hydration node
+          realIndex = idxMap![prevDynamicCount + insertionAnchor] + indexOffset
+          node = insertionParent!.childNodes[realIndex]
+        }
       }
-      hydrationState.appendAnchor = node
+      insertionParent!.$appendIndex = (node as ChildItem).$idx
     }
 
-    hydrationState.prevDynamicCount++
+    insertionParent!.$prevDynamicCount = prevDynamicCount + 1
   } else {
     node = currentHydrationNode
     if (insertionParent && (!node || node.parentNode !== insertionParent)) {
index a48ecf5bbb56332edc2b86bab73bd0f633de5a2c..df291e40e7c3690d93a617ffc507e7a926464bf3 100644 (file)
@@ -1,10 +1,6 @@
 /* @__NO_SIDE_EFFECTS__ */
 
-import {
-  type ChildItem,
-  type InsertionParent,
-  getHydrationState,
-} from '../insertionState'
+import type { ChildItem, InsertionParent } from '../insertionState'
 
 export function createElement(tagName: string): HTMLElement {
   return document.createElement(tagName)
@@ -70,21 +66,27 @@ export function _nthChild(node: InsertionParent, i: number): Node {
  */
 /* @__NO_SIDE_EFFECTS__ */
 export function __nthChild(node: Node, i: number): Node {
-  const hydrationState = getHydrationState(node as ParentNode)
-  if (hydrationState) {
-    const { prevDynamicCount, uniqueAnchorCount, logicalChildren } =
-      hydrationState
+  const parent = node as InsertionParent
+  if (parent.$idxMap) {
+    const {
+      $prevDynamicCount: prevDynamicCount = 0,
+      $anchorCount: anchorCount = 0,
+      $idxMap: idxMap,
+      $indexOffset: indexOffset = 0,
+    } = parent
     // prevDynamicCount tracks how many dynamic nodes have been processed
     // so far (prepend/insert/append).
     // For anchor-based insert, the first time an anchor is used we adopt the
-    // anchor node itself and do NOT consume the next child in `logicalChildren`,
+    // anchor node itself and do NOT consume the next child in `idxMap`,
     // yet prevDynamicCount is still incremented. This overcounts the base
     // offset by 1 per unique anchor that has appeared.
-    // uniqueAnchorCount equals the number of unique anchors seen, so we
+    // anchorCount equals the number of unique anchors seen, so we
     // subtract it to neutralize those "first-use doesn't consume" cases:
-    //   base = prevDynamicCount - uniqueAnchorCount
-    // Then index from this base: logicalChildren[base + i].
-    return logicalChildren[prevDynamicCount - uniqueAnchorCount + i]
+    //   base = prevDynamicCount - anchorCount
+    // Then index from this base: idxMap[base + i] + indexOffset.
+    const logicalIndex = prevDynamicCount - anchorCount + i
+    const realIndex = idxMap[logicalIndex] + indexOffset
+    return node.childNodes[realIndex]
   }
   return node.childNodes[i]
 }
@@ -100,11 +102,13 @@ export function _next(node: Node): Node {
  */
 /* @__NO_SIDE_EFFECTS__ */
 export function __next(node: Node): Node {
-  const hydrationState = getHydrationState(node.parentNode!)
-  if (hydrationState) {
-    const { logicalChildren } = hydrationState
+  const parent = node.parentNode! as InsertionParent
+  if (parent.$idxMap) {
+    const { $idxMap: idxMap, $indexOffset: indexOffset = 0 } = parent
     const { $idx, $uc: usedCount = 0 } = node as ChildItem
-    return logicalChildren[$idx + usedCount + 1]
+    const logicalIndex = $idx + usedCount + 1
+    const realIndex = idxMap[logicalIndex] + indexOffset
+    return node.parentNode!.childNodes[realIndex]
   }
   return node.nextSibling!
 }
index ccc941e299da2d544f6a9557d189d7d5a9658cef..d9a38fc39d46180847be28728ee6d780509c8385 100644 (file)
@@ -275,6 +275,8 @@ export function optimizePropertyLookup(): void {
   proto.$transition = undefined
   proto.$key = undefined
   proto.$evtclick = undefined
+  proto.$children = undefined
+  proto.$idx = undefined
   proto.$root = false
   proto.$html =
     proto.$txt =
index e2acbc96e66837cc8e6a1ad02829892f134dfdd1..9c4b3ca1d355ce489af887c077f825d278145714 100644 (file)
@@ -3,11 +3,22 @@ import { child, createElement, createTextNode } from './node'
 
 let t: HTMLTemplateElement
 
+export let currentTemplateFn: (Function & { $idxMap?: number[] }) | undefined =
+  undefined
+
+export function resetTemplateFn(): void {
+  currentTemplateFn = undefined
+}
+
 /*! #__NO_SIDE_EFFECTS__ */
-export function template(html: string, root?: boolean) {
+export function template(
+  html: string,
+  root?: boolean,
+): () => Node & { $root?: true } {
   let node: Node
-  return (): Node & { $root?: true } => {
+  const fn = () => {
     if (isHydrating) {
+      currentTemplateFn = fn
       if (__DEV__ && !currentHydrationNode) {
         // TODO this should not happen
         throw new Error('No current hydration node')
@@ -32,4 +43,5 @@ export function template(html: string, root?: boolean) {
     if (root) (ret as any).$root = true
     return ret
   }
+  return fn
 }
index 58f9d93799b34fed4e201cf117446d267c8833f8..b13458cfdf6dc0bd41fa82f0eb184a7fa99501fa 100644 (file)
@@ -24,6 +24,7 @@ import {
 } from './components/Transition'
 import { type VaporComponentInstance, isVaporComponent } from './component'
 import { isArray } from '@vue/shared'
+import { incrementIndexOffset } from './insertionState'
 
 export class VaporFragment<T extends Block = Block>
   implements TransitionOptions
@@ -185,6 +186,8 @@ export class DynamicFragment extends VaporFragment {
       (this.anchor = createComment(this.anchorLabel!)),
       nextSibling,
     )
+    // increment index offset since we dynamically inserted a comment node
+    incrementIndexOffset(parentNode!)
     advanceHydrationNode(this.anchor)
   }
 }
index baf9854a4e41096f1cbef2102ed8e884d0d998ee..f1e128d42edc451b4d24920e6acf65f106b25639 100644 (file)
@@ -1,22 +1,27 @@
 import { isComment, isHydrating } from './dom/hydration'
+import { currentTemplateFn, resetTemplateFn } from './dom/template'
 export type ChildItem = ChildNode & {
   $idx: number
   // used count as an anchor
   $uc?: number
 }
-export type InsertionParent = ParentNode & { $children?: ChildItem[] }
-type HydrationState = {
-  // static nodes and the start anchors of fragments
-  logicalChildren: ChildItem[]
+
+export type InsertionParent = ParentNode & {
+  $children?: ChildItem[]
+  /**
+   * hydration-specific properties
+   */
+  // mapping from logical index to real index in childNodes
+  $idxMap?: number[]
   // hydrated dynamic children count so far
-  prevDynamicCount: number
+  $prevDynamicCount?: number
   // number of unique insertion anchors that have appeared
-  uniqueAnchorCount: number
-  // current append anchor
-  appendAnchor: Node | null
+  $anchorCount?: number
+  // last append index
+  $appendIndex?: number | null
+  // number of dynamically inserted nodes (e.g., comment anchors)
+  $indexOffset?: number
 }
-
-const hydrationStateCache = new WeakMap<ParentNode, HydrationState>()
 export let insertionParent: InsertionParent | undefined
 export let insertionAnchor: Node | 0 | undefined | null
 
@@ -35,6 +40,7 @@ export function setInsertionState(
     if (isHydrating) {
       insertionAnchor = anchor as Node
       initializeHydrationState(parent)
+      resetTemplateFn()
     } else {
       // special handling append anchor value to null
       insertionAnchor =
@@ -46,12 +52,13 @@ export function setInsertionState(
   }
 }
 
-function initializeHydrationState(parent: ParentNode) {
-  if (!hydrationStateCache.has(parent)) {
+function initializeHydrationState(parent: InsertionParent) {
+  if (!parent.$idxMap) {
     const childNodes = parent.childNodes
     const len = childNodes.length
 
-    // fast path for single child case. No need to build logicalChildren
+    // fast path for single child case. use first child as hydration node
+    // no need to build logical index map
     if (
       len === 1 ||
       (len === 3 &&
@@ -62,48 +69,65 @@ function initializeHydrationState(parent: ParentNode) {
       return
     }
 
-    const logicalChildren = new Array(len) as ChildItem[]
-    // Build logical children:
-    // - static node: keep the node as a child
-    // - fragment: keep only the start anchor ('<!--[-->') as a child
-    let index = 0
-    for (let i = 0; i < len; i++) {
-      const n = childNodes[i] as ChildItem
-      n.$idx = index
-      if (n.nodeType === 8) {
-        const data = (n as any as Comment).data
-        // vdom fragment
-        if (data === '[') {
-          logicalChildren[index++] = n
-          // find matching end anchor, accounting for nested fragments
-          let depth = 1
-          let j = i + 1
-          for (; j < len; j++) {
-            const c = childNodes[j] as Comment
-            if (c.nodeType === 8) {
-              const d = c.data
-              if (d === '[') depth++
-              else if (d === ']') {
-                depth--
-                if (depth === 0) break
-              }
+    if (currentTemplateFn) {
+      if (currentTemplateFn.$idxMap) {
+        const idxMap = (parent.$idxMap = currentTemplateFn.$idxMap)
+        // set $idx to childNodes
+        for (let i = 0; i < idxMap.length; i++) {
+          ;(childNodes[idxMap[i]] as ChildItem).$idx = i
+        }
+      } else {
+        parent.$idxMap = currentTemplateFn.$idxMap = buildLogicalIndexMap(
+          len,
+          childNodes,
+        )
+      }
+    } else {
+      parent.$idxMap = buildLogicalIndexMap(len, childNodes)
+    }
+    parent.$prevDynamicCount = 0
+    parent.$anchorCount = 0
+    parent.$appendIndex = null
+    parent.$indexOffset = 0
+  }
+}
+
+function buildLogicalIndexMap(len: number, childNodes: NodeListOf<ChildNode>) {
+  const idxMap = new Array() as number[]
+  // Build logical index map:
+  // - static node: map logical index to real index
+  // - fragment: map logical index to start anchor's real index
+  let logicalIndex = 0
+  for (let i = 0; i < len; i++) {
+    const n = childNodes[i] as ChildItem
+    n.$idx = logicalIndex
+    if (n.nodeType === 8) {
+      const data = (n as any as Comment).data
+      // vdom fragment
+      if (data === '[') {
+        idxMap[logicalIndex++] = i
+        // find matching end anchor, accounting for nested fragments
+        let depth = 1
+        let j = i + 1
+        for (; j < len; j++) {
+          const c = childNodes[j] as Comment
+          if (c.nodeType === 8) {
+            const d = c.data
+            if (d === '[') depth++
+            else if (d === ']') {
+              depth--
+              if (depth === 0) break
             }
           }
-          // jump i to the end anchor
-          i = j
-          continue
         }
+        // jump i to the end anchor
+        i = j
+        continue
       }
-      logicalChildren[index++] = n
     }
-    logicalChildren.length = index
-    hydrationStateCache.set(parent, {
-      logicalChildren,
-      prevDynamicCount: 0,
-      uniqueAnchorCount: 0,
-      appendAnchor: null,
-    })
+    idxMap[logicalIndex++] = i
   }
+  return idxMap
 }
 
 function cacheTemplateChildren(parent: InsertionParent) {
@@ -124,8 +148,8 @@ export function resetInsertionState(): void {
   insertionParent = insertionAnchor = undefined
 }
 
-export function getHydrationState(
-  parent: ParentNode,
-): HydrationState | undefined {
-  return hydrationStateCache.get(parent)
+export function incrementIndexOffset(parent: InsertionParent): void {
+  if (parent.$indexOffset !== undefined) {
+    parent.$indexOffset++
+  }
 }