]> git.ipfire.org Git - thirdparty/vuejs/core.git/commitdiff
perf: enhance hydration handling and introduce logical child updates
authordaiwei <daiwei521@126.com>
Thu, 16 Oct 2025 07:27:00 +0000 (15:27 +0800)
committerdaiwei <daiwei521@126.com>
Thu, 16 Oct 2025 07:27:00 +0000 (15:27 +0800)
packages/runtime-vapor/__tests__/hydration.spec.ts
packages/runtime-vapor/src/apiCreateFor.ts
packages/runtime-vapor/src/block.ts
packages/runtime-vapor/src/componentSlots.ts
packages/runtime-vapor/src/dom/hydration.ts
packages/runtime-vapor/src/dom/node.ts
packages/runtime-vapor/src/fragment.ts
packages/runtime-vapor/src/insertionState.ts
packages/runtime-vapor/src/vdomInterop.ts

index 994a8f6ab4c12f87b0759aebdd089a9ac3159b98..2a3da918e986a5431b4d80efcdf7289506f7b0e7 100644 (file)
@@ -1452,8 +1452,8 @@ describe('Vapor Mode hydration', () => {
       expect(formatHtml(container.innerHTML)).toMatchInlineSnapshot(
         `
         "<div>
-        <!--[--><div>true</div>-true-<!--if--><!--]-->
-        </div>"
+        <!--[--><div>true</div>-true-<!--]-->
+        <!--if--></div>"
       `,
       )
 
@@ -1462,8 +1462,8 @@ describe('Vapor Mode hydration', () => {
       expect(formatHtml(container.innerHTML)).toMatchInlineSnapshot(
         `
         "<div>
-        <!--[--><!--if--><!--]-->
-        </div>"
+        <!--[--><!--]-->
+        <!--if--></div>"
       `,
       )
 
@@ -1472,8 +1472,8 @@ describe('Vapor Mode hydration', () => {
       expect(formatHtml(container.innerHTML)).toMatchInlineSnapshot(
         `
         "<div>
-        <!--[--><div>true</div>-true-<!--if--><!--]-->
-        </div>"
+        <!--[--><!--]-->
+        <div>true</div>-true-<!--if--></div>"
       `,
       )
     })
@@ -1496,8 +1496,8 @@ describe('Vapor Mode hydration', () => {
       expect(formatHtml(container.innerHTML)).toMatchInlineSnapshot(
         `
         "<div><span></span>
-        <!--[--><div>true</div>-true-<!--if--><!--]-->
-        <span></span></div>"
+        <!--[--><div>true</div>-true-<!--]-->
+        <!--if--><span></span></div>"
       `,
       )
 
@@ -1506,8 +1506,8 @@ describe('Vapor Mode hydration', () => {
       expect(formatHtml(container.innerHTML)).toMatchInlineSnapshot(
         `
         "<div><span></span>
-        <!--[--><!--if--><!--]-->
-        <span></span></div>"
+        <!--[--><!--]-->
+        <!--if--><span></span></div>"
       `,
       )
 
@@ -1515,8 +1515,8 @@ describe('Vapor Mode hydration', () => {
       await nextTick()
       expect(formatHtml(container.innerHTML)).toMatchInlineSnapshot(`
         "<div><span></span>
-        <!--[--><div>true</div>-true-<!--if--><!--]-->
-        <span></span></div>"
+        <!--[--><!--]-->
+        <div>true</div>-true-<!--if--><span></span></div>"
       `)
     })
 
@@ -1539,9 +1539,10 @@ describe('Vapor Mode hydration', () => {
       expect(formatHtml(container.innerHTML)).toMatchInlineSnapshot(
         `
         "<div><span></span>
-        <!--[--><div>true</div>-true-<!--if--><!--]-->
-        <!--[--><div>true</div>-true-<!--if--><!--]-->
-        <span></span></div>"
+        <!--[--><div>true</div>-true-<!--]-->
+        <!--if-->
+        <!--[--><div>true</div>-true-<!--]-->
+        <!--if--><span></span></div>"
       `,
       )
 
@@ -1550,9 +1551,10 @@ describe('Vapor Mode hydration', () => {
       expect(formatHtml(container.innerHTML)).toMatchInlineSnapshot(
         `
         "<div><span></span>
-        <!--[--><!--if--><!--]-->
-        <!--[--><!--if--><!--]-->
-        <span></span></div>"
+        <!--[--><!--]-->
+        <!--if-->
+        <!--[--><!--]-->
+        <!--if--><span></span></div>"
       `,
       )
 
@@ -1560,9 +1562,10 @@ describe('Vapor Mode hydration', () => {
       await nextTick()
       expect(formatHtml(container.innerHTML)).toMatchInlineSnapshot(`
         "<div><span></span>
-        <!--[--><div>true</div>-true-<!--if--><!--]-->
-        <!--[--><div>true</div>-true-<!--if--><!--]-->
-        <span></span></div>"
+        <!--[--><!--]-->
+        <div>true</div>-true-<!--if-->
+        <!--[--><!--]-->
+        <div>true</div>-true-<!--if--><span></span></div>"
       `)
     })
 
@@ -1879,8 +1882,8 @@ describe('Vapor Mode hydration', () => {
         <!--[-->
         <!--[--><div>foo</div>-bar-<!--]-->
         <!--[--><div>foo</div>-bar-<!--]-->
-        <!--[--><div>foo</div>-bar-<div>foo</div>-bar-<!--]-->
-        <!--]-->
+        <!--[--><div>foo</div>-bar-<!--]-->
+        <div>foo</div>-bar-<!--]-->
         </div>"
       `,
       )
index bdeccce2a520cd9256f3bc4ecce4897cbf57233d..38c60b99168e3541f9393061c8d953b019fdcab1 100644 (file)
@@ -12,7 +12,11 @@ import {
   watch,
 } from '@vue/reactivity'
 import { isArray, isObject, isString } from '@vue/shared'
-import { createComment, createTextNode } from './dom/node'
+import {
+  createComment,
+  createTextNode,
+  updateLastLogicalChild,
+} from './dom/node'
 import {
   type Block,
   insert,
@@ -33,9 +37,8 @@ import {
   locateHydrationNode,
   setCurrentHydrationNode,
 } from './dom/hydration'
-import { ForFragment, VaporFragment, findLastChild } from './fragment'
+import { ForFragment, VaporFragment, findBlockNode } from './fragment'
 import {
-  type ChildItem,
   insertionAnchor,
   insertionParent,
   resetInsertionState,
@@ -135,7 +138,7 @@ export const createFor = (
       for (let i = 0; i < newLength; i++) {
         const nodes = mount(source, i).nodes
         if (isHydrating) {
-          setCurrentHydrationNode(findLastChild(nodes!)!.nextSibling)
+          setCurrentHydrationNode(findBlockNode(nodes!).nextNode)
         }
       }
 
@@ -147,13 +150,9 @@ export const createFor = (
         if (!parentAnchor || (parentAnchor && !isComment(parentAnchor, ']'))) {
           throw new Error(`v-for fragment anchor node was not found.`)
         }
-        // the lastLogicalChild is the fragment start anchor; replacing it with end anchor
-        // can avoid the call to locateEndAnchor within locateChildByLogicalIndex
-        if (_insertionParent && _insertionParent!.$llc) {
-          ;(parentAnchor as any as ChildItem).$idx = (
-            _insertionParent!.$llc as ChildItem
-          ).$idx
-          _insertionParent.$llc = parentAnchor
+
+        if (_insertionParent) {
+          updateLastLogicalChild(_insertionParent!, parentAnchor)
         }
       }
     } else {
index 9dad78cb2f68fa91a86fc39fd2df0f696fb0b5f9..765b017898a1fe295c8e9a88a8126fc180d2647d 100644 (file)
@@ -166,6 +166,17 @@ export function normalizeAnchor(node: Block): Node | undefined {
   }
 }
 
+export function isFragmentBlock(block: Block): boolean {
+  if (isArray(block)) {
+    return true
+  } else if (isVaporComponent(block)) {
+    return isFragmentBlock(block.block!)
+  } else if (isFragment(block)) {
+    return isFragmentBlock(block.nodes)
+  }
+  return false
+}
+
 /**
  * dev / test only
  */
index 52ccf18cae79b429df8bdc24100eedc1593c8a16..f01034426bd9fafbaae551b9f5af712d1c58fd50 100644 (file)
@@ -15,6 +15,7 @@ import {
   locateHydrationNode,
 } from './dom/hydration'
 import { DynamicFragment, type VaporFragment } from './fragment'
+import { updateLastLogicalChild } from './dom/node'
 
 export type RawSlots = Record<string, VaporSlot> & {
   $?: DynamicSlotSource[]
@@ -170,6 +171,9 @@ export function createSlot(
     if (fragment.insert) {
       ;(fragment as VaporFragment).hydrate!()
     }
+    if (_insertionParent) {
+      updateLastLogicalChild(_insertionParent!, fragment.anchor)
+    }
     if (_insertionAnchor !== undefined) {
       advanceHydrationNode(_insertionParent!)
     }
index 8d4cba1e023327992fa117f6cf1c08d5fb4984ef..5ad6f1e3070a6b6355376e448c2f7ca9f9037514 100644 (file)
@@ -58,6 +58,7 @@ function performHydration<T>(
     ;(Node.prototype as any).$lpn = undefined
     ;(Node.prototype as any).$lan = undefined
     ;(Node.prototype as any).$lin = undefined
+    ;(Node.prototype as any).$curIdx = undefined
 
     isOptimized = true
   }
@@ -186,6 +187,10 @@ function locateHydrationNodeImpl(): void {
           ? firstChild
           : locateChildByLogicalIndex(insertionParent!, insertionAnchor)!
     }
+
+    insertionParent!.$llc = node
+    ;(node as ChildItem).$idx = insertionParent!.$curIdx =
+      insertionParent!.$curIdx === undefined ? 0 : insertionParent!.$curIdx + 1
   } else {
     node = currentHydrationNode
     if (insertionParent && (!node || node.parentNode !== insertionParent)) {
index 4bea538fbba867bae47ee222e596f42ab874501a..13500e2084cc922b366b5e3a8140f287eff8d008 100644 (file)
@@ -163,3 +163,14 @@ export function locateChildByLogicalIndex(
 
   return null
 }
+
+// use fragment end anchor as the logical child to avoid locateEndAnchor calls
+// in locateChildByLogicalIndex
+export function updateLastLogicalChild(
+  parent: InsertionParent,
+  child: Node,
+): void {
+  if (!isComment(child, ']')) return
+  ;(child as any as ChildItem).$idx = parent.$curIdx || 0
+  parent.$llc = child
+}
index 609e90b3f05ab7f15b9ca31e54fc17d83bd93a44..9a25e717efa1348807e45c9c44146e80458877da 100644 (file)
@@ -6,6 +6,7 @@ import {
   type TransitionOptions,
   type VaporTransitionHooks,
   insert,
+  isFragmentBlock,
   isValidBlock,
   remove,
 } from './block'
@@ -178,14 +179,14 @@ export class DynamicFragment extends VaporFragment {
       }
     }
 
+    const { parentNode, nextNode } = findBlockNode(this.nodes)!
     // create an anchor
-    const { parentNode, nextSibling } = findLastChild(this)!
     queuePostFlushCb(() => {
       parentNode!.insertBefore(
         (this.anchor = __DEV__
           ? createComment(this.anchorLabel!)
           : createTextNode()),
-        nextSibling,
+        nextNode,
       )
     })
   }
@@ -243,7 +244,25 @@ function findInvalidFragment(fragment: VaporFragment): VaporFragment | null {
     : fragment
 }
 
-export function findLastChild(node: Block): Node | undefined | null {
+export function findBlockNode(block: Block): {
+  parentNode: Node | null
+  nextNode: Node | null
+} {
+  let { parentNode, nextSibling: nextNode } = findLastChild(block)!
+
+  // if nodes render as a fragment and the current nextNode is fragment
+  // end anchor, need to move to the next node
+  if (nextNode && isComment(nextNode, ']') && isFragmentBlock(block)) {
+    nextNode = nextNode.nextSibling
+  }
+
+  return {
+    parentNode,
+    nextNode,
+  }
+}
+
+function findLastChild(node: Block): Node | undefined | null {
   if (node && node instanceof Node) {
     return node
   } else if (isArray(node)) {
index 9b993ba49edac04d49d47db5db4f638b13379b63..a4bd23ed4cab64e6c5d10c072cde80c6f6150850 100644 (file)
@@ -16,6 +16,8 @@ export type InsertionParent = ParentNode & {
   $lpn?: Node | null
   // last append node
   $lan?: Node | null
+  // the logical index of current hydration node
+  $curIdx?: number
 }
 export let insertionParent: InsertionParent | undefined
 export let insertionAnchor: Node | 0 | undefined | null
index 1e88ad00ab070401ed769a2d91a830ea3ac4b063..9790cf282e885e9dc7fce9c18ce255e419dd37c0 100644 (file)
@@ -283,7 +283,7 @@ function createVDOMComponent(
     hydrateVNode(vnode, parentInstance as any)
     onScopeDispose(unmount, true)
     isMounted = true
-    frag.nodes = [vnode.el as Node]
+    frag.nodes = vnode.el as any
   }
 
   frag.insert = (parentNode, anchor, transition) => {
@@ -316,7 +316,7 @@ function createVDOMComponent(
     }
 
     // update the fragment nodes
-    frag.nodes = [vnode.el as Node]
+    frag.nodes = vnode.el as any
     simpleSetCurrentInstance(prev)
   }
 
@@ -441,6 +441,10 @@ function renderVDOMSlot(
 
   frag.hydrate = () => {
     render()
+    if (__DEV__ && isComment(currentHydrationNode!, ']')) {
+      throw new Error(`Failed to locate vdom slot anchor`)
+    }
+    frag.anchor = currentHydrationNode!
     isMounted = true
   }