]> git.ipfire.org Git - thirdparty/vuejs/core.git/commitdiff
wip: handle mixed prepend and insertionAnchor
authordaiwei <daiwei521@126.com>
Thu, 31 Jul 2025 03:53:53 +0000 (11:53 +0800)
committerdaiwei <daiwei521@126.com>
Thu, 31 Jul 2025 07:13:00 +0000 (15:13 +0800)
packages/compiler-vapor/src/generators/template.ts
packages/compiler-vapor/src/transforms/transformChildren.ts
packages/runtime-vapor/__tests__/hydration.spec.ts
packages/runtime-vapor/src/apiCreateDynamicComponent.ts
packages/runtime-vapor/src/apiCreateFor.ts
packages/runtime-vapor/src/apiCreateIf.ts
packages/runtime-vapor/src/component.ts
packages/runtime-vapor/src/componentSlots.ts
packages/runtime-vapor/src/dom/hydration.ts

index 0229649cf0805a46a6ed5687effa9ced372f1568..7239f838267145c231daff01572f41ea592841d1 100644 (file)
@@ -69,7 +69,7 @@ export function genChildren(
       continue
     }
 
-    const elementIndex = Number(index) + offset
+    const elementIndex = index + offset
     // p for "placeholder" variables that are meant for possible reuse by
     // other access paths
     const variable = id === undefined ? `p${context.block.tempId++}` : `n${id}`
@@ -82,15 +82,24 @@ export function genChildren(
         pushBlock(...genCall(helper('nthChild'), from, String(elementIndex)))
       }
     } else {
-      // offset is used to determine the child during hydration.
+      // child index is used to find the child during hydration.
       // if offset is not 0, we need to specify the offset to skip the dynamic
       // children and get the correct child.
-      let childOffset = offset === 0 ? undefined : `${Math.abs(offset)}`
+      const asAnchor = children.some(child => child.anchor === id)
+      let childIndex =
+        offset === 0
+          ? undefined
+          : // if the current node is used as insertionAnchor, subtract 1 here
+            // this ensures that insertionAnchor points to the current node itself
+            // rather than its next sibling, since insertionAnchor is used as the
+            // hydration node
+            `${asAnchor ? index - 1 : index}`
+
       if (elementIndex === 0) {
-        pushBlock(...genCall(helper('child'), from, childOffset))
+        pushBlock(...genCall(helper('child'), from, childIndex))
       } else {
         // check if there's a node that we can reuse from
-        let init = genCall(helper('child'), from, childOffset)
+        let init = genCall(helper('child'), from, childIndex)
         if (elementIndex === 1) {
           init = genCall(helper('next'), init)
         } else if (elementIndex > 1) {
index 1baf7a9428239ca22c2225e0b8941e5135c7625f..56d3caf5144f5e47cd87876ab185f912571ed54b 100644 (file)
@@ -60,6 +60,7 @@ export const transformChildren: NodeTransform = (node, context) => {
 function processDynamicChildren(context: TransformContext<ElementNode>) {
   let prevDynamics: IRDynamicInfo[] = []
   let staticCount = 0
+  let prependCount = 0
   const children = context.dynamic.children
 
   for (const [index, child] of children.entries()) {
@@ -88,6 +89,7 @@ function processDynamicChildren(context: TransformContext<ElementNode>) {
             }
           }
         } else {
+          prependCount += prevDynamics.length
           registerInsertion(prevDynamics, context, -1 /* prepend */)
         }
         prevDynamics = []
@@ -97,8 +99,8 @@ function processDynamicChildren(context: TransformContext<ElementNode>) {
   }
 
   if (prevDynamics.length) {
-    registerInsertion(prevDynamics, context, undefined)
-    context.dynamic.dynamicChildOffset = staticCount
+    registerInsertion(prevDynamics, context)
+    context.dynamic.dynamicChildOffset = staticCount + prependCount
   }
 }
 
index 8bc7689e325290ef0e772efd2c8c46ec232e4702..20b357c5088c34bbe28b4986e0de6dd320652d0b 100644 (file)
@@ -1285,6 +1285,65 @@ describe('Vapor Mode hydration', () => {
       expect(container.innerHTML).toBe(`<div>foo</div><!--if--><!--if-->`)
     })
 
+    test('mixed prepend and insertion anchor', async () => {
+      const data = reactive({
+        show: true,
+        foo: 'foo',
+        bar: 'bar',
+        qux: 'qux',
+      })
+      const { container } = await testHydration(
+        `<template>
+          <components.Child/>
+        </template>`,
+        {
+          Child: `<template>
+            <span v-if="data.show">
+              <span v-if="data.show">{{data.foo}}</span>
+              <span v-if="data.show">{{data.bar}}</span>
+              <span>baz</span>
+              <span v-if="data.show">{{data.qux}}</span>
+              <span>quux</span>
+            </span>
+          </template>`,
+        },
+        data,
+      )
+      expect(container.innerHTML).toBe(
+        `<span>` +
+          `<span>foo</span><!--if-->` +
+          `<span>bar</span><!--if-->` +
+          `<span>baz</span>` +
+          `<span>qux</span><!--if-->` +
+          `<span>quux</span>` +
+          `</span><!--if-->`,
+      )
+
+      data.qux = 'qux1'
+      await nextTick()
+      expect(container.innerHTML).toBe(
+        `<span>` +
+          `<span>foo</span><!--if-->` +
+          `<span>bar</span><!--if-->` +
+          `<span>baz</span>` +
+          `<span>qux1</span><!--if-->` +
+          `<span>quux</span>` +
+          `</span><!--if-->`,
+      )
+
+      data.foo = 'foo1'
+      await nextTick()
+      expect(container.innerHTML).toBe(
+        `<span>` +
+          `<span>foo1</span><!--if-->` +
+          `<span>bar</span><!--if-->` +
+          `<span>baz</span>` +
+          `<span>qux1</span><!--if-->` +
+          `<span>quux</span>` +
+          `</span><!--if-->`,
+      )
+    })
+
     test('v-if/else-if/else chain on component - switch branches', async () => {
       const data = ref('a')
       const { container } = await testHydration(
index 11a4d18d491f1951ecf313d75f718e1f32947fdf..1bf7767352bc59d9b12e7e1bf5c3e42bc8315767 100644 (file)
@@ -10,7 +10,7 @@ import {
   resetInsertionState,
 } from './insertionState'
 import { DYNAMIC_COMPONENT_ANCHOR_LABEL } from '@vue/shared'
-import { isHydrating } from './dom/hydration'
+import { advanceHydrationNode, isHydrating } from './dom/hydration'
 import { DynamicFragment, type VaporFragment } from './fragment'
 
 export function createDynamicComponent(
@@ -52,5 +52,8 @@ export function createDynamicComponent(
   if (!isHydrating && _insertionParent) {
     insert(frag, _insertionParent, _insertionAnchor)
   }
+  if (isHydrating && _insertionAnchor !== undefined) {
+    advanceHydrationNode(_insertionParent!)
+  }
   return frag
 }
index 6b9482ffe06e209eab6534b955b801ec9ff31d7a..5f3e9ea80ee4362d355ec36b6f7231daedd11377 100644 (file)
@@ -20,6 +20,7 @@ import type { DynamicSlot } from './componentSlots'
 import { renderEffect } from './renderEffect'
 import { VaporVForFlags } from '../../shared/src/vaporFlags'
 import {
+  advanceHydrationNode,
   currentHydrationNode,
   isHydrating,
   locateHydrationNode,
@@ -468,6 +469,9 @@ export const createFor = (
   if (!isHydrating && _insertionParent) {
     insert(frag, _insertionParent, _insertionAnchor)
   }
+  if (isHydrating && _insertionAnchor !== undefined) {
+    advanceHydrationNode(_insertionParent!)
+  }
 
   return frag
 
index ad8d4019f9295d50d2c014b34fa5ed81550ee591..4ed055554d577cba03808902b11a748a6cd8c372 100644 (file)
@@ -1,6 +1,6 @@
 import { IF_ANCHOR_LABEL } from '@vue/shared'
 import { type Block, type BlockFn, insert } from './block'
-import { isHydrating } from './dom/hydration'
+import { advanceHydrationNode, isHydrating } from './dom/hydration'
 import {
   insertionAnchor,
   insertionParent,
@@ -78,5 +78,12 @@ export function createIf(
     insert(frag, _insertionParent, _insertionAnchor)
   }
 
+  // if _insertionAnchor is defined, insertionParent contains a static node
+  // that should be skipped during hydration.
+  // Advance to the next sibling node to bypass this static node.
+  if (isHydrating && _insertionAnchor !== undefined) {
+    advanceHydrationNode(_insertionParent!)
+  }
+
   return frag
 }
index f8980f9ac05ff3b62cd6389cb8c1a446717a954c..f9bac4347f4c9bcad40c3e524fccf28aa9b8d637 100644 (file)
@@ -67,6 +67,7 @@ import { hmrReload, hmrRerender } from './hmr'
 import { createElement } from './dom/node'
 import {
   adoptTemplate,
+  advanceHydrationNode,
   currentHydrationNode,
   isHydrating,
   locateHydrationNode,
@@ -182,6 +183,9 @@ export function createComponent(
     if (_insertionParent) {
       insert(frag, _insertionParent, _insertionAnchor)
     }
+    if (isHydrating && _insertionAnchor !== undefined) {
+      advanceHydrationNode(_insertionParent!)
+    }
     return frag
   }
 
@@ -194,6 +198,10 @@ export function createComponent(
       frag.hydrate()
     }
 
+    if (isHydrating && _insertionAnchor !== undefined) {
+      advanceHydrationNode(_insertionParent!)
+    }
+
     return frag as any
   }
 
@@ -587,6 +595,10 @@ export function createComponentWithFallback(
     insert(el, _insertionParent, _insertionAnchor)
   }
 
+  if (isHydrating && _insertionAnchor !== undefined) {
+    advanceHydrationNode(_insertionParent!)
+  }
+
   return el
 }
 
index 4fcb86a39fac611fd347f8582636ca54c31f77c5..9ae3bd2497f8a131faa2ef1ec828dc5b9be42179 100644 (file)
@@ -16,7 +16,7 @@ import {
   insertionParent,
   resetInsertionState,
 } from './insertionState'
-import { isHydrating } from './dom/hydration'
+import { advanceHydrationNode, isHydrating } from './dom/hydration'
 import { DynamicFragment } from './fragment'
 
 export type RawSlots = Record<string, VaporSlot> & {
@@ -175,6 +175,10 @@ export function createSlot(
     insert(fragment, _insertionParent, _insertionAnchor)
   }
 
+  if (isHydrating && _insertionAnchor !== undefined) {
+    advanceHydrationNode(_insertionParent!)
+  }
+
   return fragment
 }
 
index f90f153a2ad116f83da75e69f4c68e9c0a563add..fffdee6a573cf5df40d267ee0c2ef25c0e131436 100644 (file)
@@ -8,7 +8,6 @@ import {
 import {
   __next,
   __nthChild,
-  _nthChild,
   disableHydrationNodeLookup,
   enableHydrationNodeLookup,
 } from './node'
@@ -137,7 +136,7 @@ function locateHydrationNodeImpl(isFragment?: boolean): void {
     if (insertionParent && (!node || node.parentNode !== insertionParent)) {
       node =
         childToHydrateMap.get(insertionParent) ||
-        _nthChild(insertionParent, insertionParent.$dp || 0)
+        __nthChild(insertionParent, insertionParent.$dp || 0)
     }
 
     // locate slot fragment start anchor