From: daiwei Date: Thu, 31 Jul 2025 03:53:53 +0000 (+0800) Subject: wip: handle mixed prepend and insertionAnchor X-Git-Url: http://git.ipfire.org/?a=commitdiff_plain;h=a9b911e25412d983b66959097f58912cd2d9df72;p=thirdparty%2Fvuejs%2Fcore.git wip: handle mixed prepend and insertionAnchor --- diff --git a/packages/compiler-vapor/src/generators/template.ts b/packages/compiler-vapor/src/generators/template.ts index 0229649cf0..7239f83826 100644 --- a/packages/compiler-vapor/src/generators/template.ts +++ b/packages/compiler-vapor/src/generators/template.ts @@ -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) { diff --git a/packages/compiler-vapor/src/transforms/transformChildren.ts b/packages/compiler-vapor/src/transforms/transformChildren.ts index 1baf7a9428..56d3caf514 100644 --- a/packages/compiler-vapor/src/transforms/transformChildren.ts +++ b/packages/compiler-vapor/src/transforms/transformChildren.ts @@ -60,6 +60,7 @@ export const transformChildren: NodeTransform = (node, context) => { function processDynamicChildren(context: TransformContext) { 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) { } } } else { + prependCount += prevDynamics.length registerInsertion(prevDynamics, context, -1 /* prepend */) } prevDynamics = [] @@ -97,8 +99,8 @@ function processDynamicChildren(context: TransformContext) { } if (prevDynamics.length) { - registerInsertion(prevDynamics, context, undefined) - context.dynamic.dynamicChildOffset = staticCount + registerInsertion(prevDynamics, context) + context.dynamic.dynamicChildOffset = staticCount + prependCount } } diff --git a/packages/runtime-vapor/__tests__/hydration.spec.ts b/packages/runtime-vapor/__tests__/hydration.spec.ts index 8bc7689e32..20b357c508 100644 --- a/packages/runtime-vapor/__tests__/hydration.spec.ts +++ b/packages/runtime-vapor/__tests__/hydration.spec.ts @@ -1285,6 +1285,65 @@ describe('Vapor Mode hydration', () => { expect(container.innerHTML).toBe(`
foo
`) }) + test('mixed prepend and insertion anchor', async () => { + const data = reactive({ + show: true, + foo: 'foo', + bar: 'bar', + qux: 'qux', + }) + const { container } = await testHydration( + ``, + { + Child: ``, + }, + data, + ) + expect(container.innerHTML).toBe( + `` + + `foo` + + `bar` + + `baz` + + `qux` + + `quux` + + ``, + ) + + data.qux = 'qux1' + await nextTick() + expect(container.innerHTML).toBe( + `` + + `foo` + + `bar` + + `baz` + + `qux1` + + `quux` + + ``, + ) + + data.foo = 'foo1' + await nextTick() + expect(container.innerHTML).toBe( + `` + + `foo1` + + `bar` + + `baz` + + `qux1` + + `quux` + + ``, + ) + }) + test('v-if/else-if/else chain on component - switch branches', async () => { const data = ref('a') const { container } = await testHydration( diff --git a/packages/runtime-vapor/src/apiCreateDynamicComponent.ts b/packages/runtime-vapor/src/apiCreateDynamicComponent.ts index 11a4d18d49..1bf7767352 100644 --- a/packages/runtime-vapor/src/apiCreateDynamicComponent.ts +++ b/packages/runtime-vapor/src/apiCreateDynamicComponent.ts @@ -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 } diff --git a/packages/runtime-vapor/src/apiCreateFor.ts b/packages/runtime-vapor/src/apiCreateFor.ts index 6b9482ffe0..5f3e9ea80e 100644 --- a/packages/runtime-vapor/src/apiCreateFor.ts +++ b/packages/runtime-vapor/src/apiCreateFor.ts @@ -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 diff --git a/packages/runtime-vapor/src/apiCreateIf.ts b/packages/runtime-vapor/src/apiCreateIf.ts index ad8d4019f9..4ed055554d 100644 --- a/packages/runtime-vapor/src/apiCreateIf.ts +++ b/packages/runtime-vapor/src/apiCreateIf.ts @@ -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 } diff --git a/packages/runtime-vapor/src/component.ts b/packages/runtime-vapor/src/component.ts index f8980f9ac0..f9bac4347f 100644 --- a/packages/runtime-vapor/src/component.ts +++ b/packages/runtime-vapor/src/component.ts @@ -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 } diff --git a/packages/runtime-vapor/src/componentSlots.ts b/packages/runtime-vapor/src/componentSlots.ts index 4fcb86a39f..9ae3bd2497 100644 --- a/packages/runtime-vapor/src/componentSlots.ts +++ b/packages/runtime-vapor/src/componentSlots.ts @@ -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 & { @@ -175,6 +175,10 @@ export function createSlot( insert(fragment, _insertionParent, _insertionAnchor) } + if (isHydrating && _insertionAnchor !== undefined) { + advanceHydrationNode(_insertionParent!) + } + return fragment } diff --git a/packages/runtime-vapor/src/dom/hydration.ts b/packages/runtime-vapor/src/dom/hydration.ts index f90f153a2a..fffdee6a57 100644 --- a/packages/runtime-vapor/src/dom/hydration.ts +++ b/packages/runtime-vapor/src/dom/hydration.ts @@ -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