From: daiwei Date: Fri, 4 Jul 2025 02:52:45 +0000 (+0800) Subject: fix(hydration): skip dynamic children in __child X-Git-Url: http://git.ipfire.org/?a=commitdiff_plain;h=3f3480c05bf8b4c62e18e2be956c0edc406b726c;p=thirdparty%2Fvuejs%2Fcore.git fix(hydration): skip dynamic children in __child --- diff --git a/packages/compiler-vapor/__tests__/__snapshots__/compile.spec.ts.snap b/packages/compiler-vapor/__tests__/__snapshots__/compile.spec.ts.snap index b10a98d32c..5ed49dc4e5 100644 --- a/packages/compiler-vapor/__tests__/__snapshots__/compile.spec.ts.snap +++ b/packages/compiler-vapor/__tests__/__snapshots__/compile.spec.ts.snap @@ -157,7 +157,7 @@ export function render(_ctx, $props, $emit, $attrs, $slots) { const _component_Comp = _resolveComponent("Comp") const n0 = t0() const n3 = t1() - const n2 = _child(n3) + const n2 = _child(n3, 1) _setInsertionState(n3, 0) const n1 = _createComponentWithFallback(_component_Comp) _renderEffect(() => { diff --git a/packages/compiler-vapor/src/generators/template.ts b/packages/compiler-vapor/src/generators/template.ts index 9e2b810610..0229649cf0 100644 --- a/packages/compiler-vapor/src/generators/template.ts +++ b/packages/compiler-vapor/src/generators/template.ts @@ -82,11 +82,15 @@ export function genChildren( pushBlock(...genCall(helper('nthChild'), from, String(elementIndex))) } } else { + // offset is used to determine 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)}` if (elementIndex === 0) { - pushBlock(...genCall(helper('child'), from)) + pushBlock(...genCall(helper('child'), from, childOffset)) } else { // check if there's a node that we can reuse from - let init = genCall(helper('child'), from) + let init = genCall(helper('child'), from, childOffset) if (elementIndex === 1) { init = genCall(helper('next'), init) } else if (elementIndex > 1) { diff --git a/packages/runtime-vapor/__tests__/hydration.spec.ts b/packages/runtime-vapor/__tests__/hydration.spec.ts index 42f3add505..e854a19360 100644 --- a/packages/runtime-vapor/__tests__/hydration.spec.ts +++ b/packages/runtime-vapor/__tests__/hydration.spec.ts @@ -2176,6 +2176,43 @@ describe('Vapor Mode hydration', () => { ) }) + test('mixed consecutive slot and element', async () => { + const data = reactive({ + text: 'foo', + msg: 'hi', + }) + const { container } = await testHydration( + ``, + { + Child: ``, + }, + data, + ) + + expect(container.innerHTML).toBe( + `
` + + `foo` + + `bar` + + `
hi
` + + `
`, + ) + + data.msg = 'bar' + await nextTick() + expect(container.innerHTML).toBe( + `
` + + `foo` + + `bar` + + `
bar
` + + `
`, + ) + }) + test('mixed slot and element', async () => { const data = reactive({ text: 'foo', diff --git a/packages/runtime-vapor/src/dom/node.ts b/packages/runtime-vapor/src/dom/node.ts index 4c613324e1..7622c1676d 100644 --- a/packages/runtime-vapor/src/dom/node.ts +++ b/packages/runtime-vapor/src/dom/node.ts @@ -41,7 +41,7 @@ export function _child(node: ParentNode): Node { * * Client Compiled Code (Simplified): * const n2 = t0() // n2 = `
` - * const n1 = _child(n2) // n1 = text node + * const n1 = _child(n2, 1) // n1 = text node * // ... slot creation ... * _renderEffect(() => _setText(n1, _ctx.msg)) * @@ -49,18 +49,18 @@ export function _child(node: ParentNode): Node { * * Hydration Mismatch: * - During hydration, `n2` refers to the SSR `
`. - * - `_child(n2)` would return ``. + * - `_child(n2, 1)` would return ``. * - The client code expects `n1` to be the text node, but gets the comment. * The subsequent `_setText(n1, ...)` would fail or target the wrong node. * * Solution (`__child`): - * - `__child(n2)` is used during hydration. It skips the SSR fragment anchors - * (`...`) and any other non-content nodes to find the - * "Actual Text Node", correctly matching the client's expectation for `n1`. + * - `__child(n2, offset)` is used during hydration. It skips the dynamic children + * to find the "Actual Text Node", correctly matching the client's expectation + * for `n1`. */ /*! #__NO_SIDE_EFFECTS__ */ -export function __child(node: ParentNode): Node { - let n = node.firstChild! +export function __child(node: ParentNode, offset?: number): Node { + let n = offset ? __nthChild(node, offset) : node.firstChild! if (isComment(n, '[')) { n = locateEndAnchor(n)!.nextSibling! @@ -162,8 +162,8 @@ type DelegatedFunction any> = T & { } /*! #__NO_SIDE_EFFECTS__ */ -export const child: DelegatedFunction = node => { - return child.impl(node) +export const child: DelegatedFunction = (node, offset) => { + return child.impl(node, offset) } child.impl = _child