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
}
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) {

View 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
}
}

View 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(

View 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
}

View 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

View 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
}

View 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
}

View 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
}

View 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