wip: handle mixed prepend and insertionAnchor
ci / test (push) Waiting to run Details
ci / continuous-release (push) Waiting to run Details

This commit is contained in:
daiwei 2025-07-31 11:53:53 +08:00
parent 6606cbdb4b
commit a9b911e254
9 changed files with 111 additions and 12 deletions

View File

@ -69,7 +69,7 @@ export function genChildren(
continue continue
} }
const elementIndex = Number(index) + offset const elementIndex = index + offset
// p for "placeholder" variables that are meant for possible reuse by // p for "placeholder" variables that are meant for possible reuse by
// other access paths // other access paths
const variable = id === undefined ? `p${context.block.tempId++}` : `n${id}` const variable = id === undefined ? `p${context.block.tempId++}` : `n${id}`
@ -82,15 +82,24 @@ export function genChildren(
pushBlock(...genCall(helper('nthChild'), from, String(elementIndex))) pushBlock(...genCall(helper('nthChild'), from, String(elementIndex)))
} }
} else { } 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 // if offset is not 0, we need to specify the offset to skip the dynamic
// children and get the correct child. // 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) { if (elementIndex === 0) {
pushBlock(...genCall(helper('child'), from, childOffset)) pushBlock(...genCall(helper('child'), from, childIndex))
} else { } else {
// check if there's a node that we can reuse from // 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) { if (elementIndex === 1) {
init = genCall(helper('next'), init) init = genCall(helper('next'), init)
} else if (elementIndex > 1) { } else if (elementIndex > 1) {

View File

@ -60,6 +60,7 @@ export const transformChildren: NodeTransform = (node, context) => {
function processDynamicChildren(context: TransformContext<ElementNode>) { function processDynamicChildren(context: TransformContext<ElementNode>) {
let prevDynamics: IRDynamicInfo[] = [] let prevDynamics: IRDynamicInfo[] = []
let staticCount = 0 let staticCount = 0
let prependCount = 0
const children = context.dynamic.children const children = context.dynamic.children
for (const [index, child] of children.entries()) { for (const [index, child] of children.entries()) {
@ -88,6 +89,7 @@ function processDynamicChildren(context: TransformContext<ElementNode>) {
} }
} }
} else { } else {
prependCount += prevDynamics.length
registerInsertion(prevDynamics, context, -1 /* prepend */) registerInsertion(prevDynamics, context, -1 /* prepend */)
} }
prevDynamics = [] prevDynamics = []
@ -97,8 +99,8 @@ function processDynamicChildren(context: TransformContext<ElementNode>) {
} }
if (prevDynamics.length) { if (prevDynamics.length) {
registerInsertion(prevDynamics, context, undefined) registerInsertion(prevDynamics, context)
context.dynamic.dynamicChildOffset = staticCount context.dynamic.dynamicChildOffset = staticCount + prependCount
} }
} }

View File

@ -1285,6 +1285,65 @@ describe('Vapor Mode hydration', () => {
expect(container.innerHTML).toBe(`<div>foo</div><!--if--><!--if-->`) 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 () => { test('v-if/else-if/else chain on component - switch branches', async () => {
const data = ref('a') const data = ref('a')
const { container } = await testHydration( const { container } = await testHydration(

View File

@ -10,7 +10,7 @@ import {
resetInsertionState, resetInsertionState,
} from './insertionState' } from './insertionState'
import { DYNAMIC_COMPONENT_ANCHOR_LABEL } from '@vue/shared' 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' import { DynamicFragment, type VaporFragment } from './fragment'
export function createDynamicComponent( export function createDynamicComponent(
@ -52,5 +52,8 @@ export function createDynamicComponent(
if (!isHydrating && _insertionParent) { if (!isHydrating && _insertionParent) {
insert(frag, _insertionParent, _insertionAnchor) insert(frag, _insertionParent, _insertionAnchor)
} }
if (isHydrating && _insertionAnchor !== undefined) {
advanceHydrationNode(_insertionParent!)
}
return frag return frag
} }

View File

@ -20,6 +20,7 @@ import type { DynamicSlot } from './componentSlots'
import { renderEffect } from './renderEffect' import { renderEffect } from './renderEffect'
import { VaporVForFlags } from '../../shared/src/vaporFlags' import { VaporVForFlags } from '../../shared/src/vaporFlags'
import { import {
advanceHydrationNode,
currentHydrationNode, currentHydrationNode,
isHydrating, isHydrating,
locateHydrationNode, locateHydrationNode,
@ -468,6 +469,9 @@ export const createFor = (
if (!isHydrating && _insertionParent) { if (!isHydrating && _insertionParent) {
insert(frag, _insertionParent, _insertionAnchor) insert(frag, _insertionParent, _insertionAnchor)
} }
if (isHydrating && _insertionAnchor !== undefined) {
advanceHydrationNode(_insertionParent!)
}
return frag return frag

View File

@ -1,6 +1,6 @@
import { IF_ANCHOR_LABEL } from '@vue/shared' import { IF_ANCHOR_LABEL } from '@vue/shared'
import { type Block, type BlockFn, insert } from './block' import { type Block, type BlockFn, insert } from './block'
import { isHydrating } from './dom/hydration' import { advanceHydrationNode, isHydrating } from './dom/hydration'
import { import {
insertionAnchor, insertionAnchor,
insertionParent, insertionParent,
@ -78,5 +78,12 @@ export function createIf(
insert(frag, _insertionParent, _insertionAnchor) 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 return frag
} }

View File

@ -67,6 +67,7 @@ import { hmrReload, hmrRerender } from './hmr'
import { createElement } from './dom/node' import { createElement } from './dom/node'
import { import {
adoptTemplate, adoptTemplate,
advanceHydrationNode,
currentHydrationNode, currentHydrationNode,
isHydrating, isHydrating,
locateHydrationNode, locateHydrationNode,
@ -182,6 +183,9 @@ export function createComponent(
if (_insertionParent) { if (_insertionParent) {
insert(frag, _insertionParent, _insertionAnchor) insert(frag, _insertionParent, _insertionAnchor)
} }
if (isHydrating && _insertionAnchor !== undefined) {
advanceHydrationNode(_insertionParent!)
}
return frag return frag
} }
@ -194,6 +198,10 @@ export function createComponent(
frag.hydrate() frag.hydrate()
} }
if (isHydrating && _insertionAnchor !== undefined) {
advanceHydrationNode(_insertionParent!)
}
return frag as any return frag as any
} }
@ -587,6 +595,10 @@ export function createComponentWithFallback(
insert(el, _insertionParent, _insertionAnchor) insert(el, _insertionParent, _insertionAnchor)
} }
if (isHydrating && _insertionAnchor !== undefined) {
advanceHydrationNode(_insertionParent!)
}
return el return el
} }

View File

@ -16,7 +16,7 @@ import {
insertionParent, insertionParent,
resetInsertionState, resetInsertionState,
} from './insertionState' } from './insertionState'
import { isHydrating } from './dom/hydration' import { advanceHydrationNode, isHydrating } from './dom/hydration'
import { DynamicFragment } from './fragment' import { DynamicFragment } from './fragment'
export type RawSlots = Record<string, VaporSlot> & { export type RawSlots = Record<string, VaporSlot> & {
@ -175,6 +175,10 @@ export function createSlot(
insert(fragment, _insertionParent, _insertionAnchor) insert(fragment, _insertionParent, _insertionAnchor)
} }
if (isHydrating && _insertionAnchor !== undefined) {
advanceHydrationNode(_insertionParent!)
}
return fragment return fragment
} }

View File

@ -8,7 +8,6 @@ import {
import { import {
__next, __next,
__nthChild, __nthChild,
_nthChild,
disableHydrationNodeLookup, disableHydrationNodeLookup,
enableHydrationNodeLookup, enableHydrationNodeLookup,
} from './node' } from './node'
@ -137,7 +136,7 @@ function locateHydrationNodeImpl(isFragment?: boolean): void {
if (insertionParent && (!node || node.parentNode !== insertionParent)) { if (insertionParent && (!node || node.parentNode !== insertionParent)) {
node = node =
childToHydrateMap.get(insertionParent) || childToHydrateMap.get(insertionParent) ||
_nthChild(insertionParent, insertionParent.$dp || 0) __nthChild(insertionParent, insertionParent.$dp || 0)
} }
// locate slot fragment start anchor // locate slot fragment start anchor