From: daiwei Date: Thu, 7 Aug 2025 07:21:37 +0000 (+0800) Subject: wip: create anchor for DynamicFragment when necessary X-Git-Url: http://git.ipfire.org/cgi-bin/gitweb.cgi?a=commitdiff_plain;h=065064011a8f7c3cd2cc84aaeb373e6471aa1cd7;p=thirdparty%2Fvuejs%2Fcore.git wip: create anchor for DynamicFragment when necessary --- diff --git a/packages/runtime-vapor/__tests__/hydration.spec.ts b/packages/runtime-vapor/__tests__/hydration.spec.ts index 6ec5f09e66..7b471ab7ae 100644 --- a/packages/runtime-vapor/__tests__/hydration.spec.ts +++ b/packages/runtime-vapor/__tests__/hydration.spec.ts @@ -1195,11 +1195,15 @@ describe('Vapor Mode hydration', () => { data.value = 'b' await nextTick() - expect(container.innerHTML).toBe(`
bar
`) + expect(container.innerHTML).toBe( + `
bar
`, + ) data.value = 'c' await nextTick() - expect(container.innerHTML).toBe(`
baz
`) + expect(container.innerHTML).toBe( + `
baz
`, + ) data.value = 'a' await nextTick() @@ -1390,13 +1394,13 @@ describe('Vapor Mode hydration', () => { data.value = 'b' await nextTick() expect(container.innerHTML).toBe( - `b child2`, + `b child2`, ) data.value = 'c' await nextTick() expect(container.innerHTML).toBe( - `c child3`, + `c child3`, ) data.value = 'a' @@ -2762,13 +2766,13 @@ describe('Vapor Mode hydration', () => { ) expect(container.innerHTML).toBe( - `
foo
`, + `
foo
`, ) data.foo = 'bar' await nextTick() expect(container.innerHTML).toBe( - `
bar
`, + `
bar
`, ) }) }) diff --git a/packages/runtime-vapor/src/apiCreateIf.ts b/packages/runtime-vapor/src/apiCreateIf.ts index ac9f1ffd9a..cec91af0a9 100644 --- a/packages/runtime-vapor/src/apiCreateIf.ts +++ b/packages/runtime-vapor/src/apiCreateIf.ts @@ -1,4 +1,4 @@ -import { IF_ANCHOR_LABEL } from '@vue/shared' +import { ELSE_IF_ANCHOR_LABEL, IF_ANCHOR_LABEL } from '@vue/shared' import { type Block, type BlockFn, insert } from './block' import { advanceHydrationNode, isHydrating } from './dom/hydration' import { @@ -56,8 +56,10 @@ export function createIf( frag = condition() ? b1() : b2 ? b2() : [] } else { frag = - (isHydrating || __DEV__) && !elseIf - ? new DynamicFragment(IF_ANCHOR_LABEL) + isHydrating || __DEV__ + ? new DynamicFragment( + elseIf && isHydrating ? ELSE_IF_ANCHOR_LABEL : IF_ANCHOR_LABEL, + ) : new DynamicFragment() if (isHydrating) { ;(frag as DynamicFragment).teardown = () => { diff --git a/packages/runtime-vapor/src/block.ts b/packages/runtime-vapor/src/block.ts index 04b0a4881c..4dd76021a0 100644 --- a/packages/runtime-vapor/src/block.ts +++ b/packages/runtime-vapor/src/block.ts @@ -14,11 +14,7 @@ import { } from '@vue/runtime-dom' import { isHydrating } from './dom/hydration' import { getInheritedScopeIds } from '@vue/runtime-dom' -import { - type DynamicFragment, - type VaporFragment, - isFragment, -} from './fragment' +import { DynamicFragment, type VaporFragment, isFragment } from './fragment' export interface TransitionOptions { $key?: any @@ -169,6 +165,19 @@ export function normalizeAnchor(node: Block): Node | undefined { } } +export function findLastChild(node: Block): Node | undefined | null { + if (node && node instanceof Node) { + return node + } else if (isArray(node)) { + return findLastChild(node[node.length - 1]) + } else if (isVaporComponent(node)) { + return findLastChild(node.block!) + } else { + if (node instanceof DynamicFragment) return node.anchor + return findLastChild(node.nodes!) + } +} + /** * dev / test only */ diff --git a/packages/runtime-vapor/src/fragment.ts b/packages/runtime-vapor/src/fragment.ts index 9cf99e38e0..7bfad15c34 100644 --- a/packages/runtime-vapor/src/fragment.ts +++ b/packages/runtime-vapor/src/fragment.ts @@ -5,6 +5,7 @@ import { type BlockFn, type TransitionOptions, type VaporTransitionHooks, + findLastChild, insert, isValidBlock, remove, @@ -22,7 +23,7 @@ import { applyTransitionLeaveHooks, } from './components/Transition' import type { VaporComponentInstance } from './component' -import { normalizeAnchor } from './block' +import { ELSE_IF_ANCHOR_LABEL } from '@vue/shared' export class VaporFragment implements TransitionOptions @@ -76,7 +77,7 @@ export class DynamicFragment extends VaporFragment { update(render?: BlockFn, key: any = render): void { if (key === this.current) { - if (isHydrating && this.anchorLabel) this.hydrate(this.anchorLabel!, true) + if (isHydrating) this.hydrate(this.anchorLabel!, true) return } this.current = key @@ -142,49 +143,43 @@ export class DynamicFragment extends VaporFragment { setActiveSub(prevSub) - if (isHydrating && this.anchorLabel) { - // skip hydration for empty forwarded slots because - // the server output does not include their anchors - if ( - this.nodes instanceof DynamicFragment && - this.nodes.forwarded && - !isValidBlock(this.nodes) - ) { - return - } - this.hydrate(this.anchorLabel) - } + if (isHydrating) this.hydrate(this.anchorLabel!) } hydrate = (label: string, isEmpty: boolean = false): void => { - // for `v-if="false"`, the node will be an empty comment, use it as the anchor. - // otherwise, find next sibling vapor fragment anchor - if (label === 'if' && isEmpty) { - this.anchor = locateVaporFragmentAnchor(currentHydrationNode!, '')! + const createAnchor = () => { + const { parentNode, nextSibling } = findLastChild(this.nodes)! + parentNode!.insertBefore( + (this.anchor = createComment(label)), + nextSibling, + ) + } + + // manually create anchors for: + // 1. else-if branch + // 2. empty forwarded slot + // (not present in SSR output) + if ( + label === ELSE_IF_ANCHOR_LABEL || + (this.nodes instanceof DynamicFragment && + this.nodes.forwarded && + !isValidBlock(this.nodes)) + ) { + createAnchor() } else { - this.anchor = locateVaporFragmentAnchor(currentHydrationNode!, label)! - // comment anchors are not included in ssr slot vnode fallback - if (!this.anchor) { - if (label === 'slot') { + // for `v-if="false"`, the node will be an empty comment, use it as the anchor. + // otherwise, find next sibling vapor fragment anchor + if (label === 'if' && isEmpty) { + this.anchor = locateVaporFragmentAnchor(currentHydrationNode!, '')! + } else { + this.anchor = locateVaporFragmentAnchor(currentHydrationNode!, label)! + if (!this.anchor && label === 'slot') { // fallback to fragment end anchor for this.anchor = locateVaporFragmentAnchor(currentHydrationNode!, ']')! - } else { - // create anchor - if (isFragment(this.nodes) && this.nodes.anchor) { - // nested vapor fragment - const { parentNode, nextSibling } = this.nodes.anchor - parentNode!.insertBefore( - (this.anchor = __DEV__ ? createComment(label) : createTextNode()), - nextSibling, - ) - } else { - const { parentNode, nextSibling } = normalizeAnchor(this.nodes)! - parentNode!.insertBefore( - (this.anchor = __DEV__ ? createComment(label) : createTextNode()), - nextSibling, - ) - } } + + // anchors are not present in ssr slot vnode fallback + if (!this.anchor) createAnchor() } } diff --git a/packages/shared/src/domAnchors.ts b/packages/shared/src/domAnchors.ts index f807ee169d..d03409a326 100644 --- a/packages/shared/src/domAnchors.ts +++ b/packages/shared/src/domAnchors.ts @@ -2,6 +2,7 @@ export const DYNAMIC_START_ANCHOR_LABEL = '[[' export const DYNAMIC_END_ANCHOR_LABEL = ']]' export const IF_ANCHOR_LABEL: string = 'if' +export const ELSE_IF_ANCHOR_LABEL: string = 'else-if' export const DYNAMIC_COMPONENT_ANCHOR_LABEL: string = 'dynamic-component' export const FOR_ANCHOR_LABEL: string = 'for' export const SLOT_ANCHOR_LABEL: string = 'slot'