From: daiwei Date: Thu, 16 Oct 2025 07:27:00 +0000 (+0800) Subject: perf: enhance hydration handling and introduce logical child updates X-Git-Url: http://git.ipfire.org/gitweb.cgi?a=commitdiff_plain;h=082b0d35e0a986f2a7c10d6ee689330bb97c2a64;p=thirdparty%2Fvuejs%2Fcore.git perf: enhance hydration handling and introduce logical child updates --- diff --git a/packages/runtime-vapor/__tests__/hydration.spec.ts b/packages/runtime-vapor/__tests__/hydration.spec.ts index 994a8f6ab4..2a3da918e9 100644 --- a/packages/runtime-vapor/__tests__/hydration.spec.ts +++ b/packages/runtime-vapor/__tests__/hydration.spec.ts @@ -1452,8 +1452,8 @@ describe('Vapor Mode hydration', () => { expect(formatHtml(container.innerHTML)).toMatchInlineSnapshot( ` "
-
true
-true- -
" +
true
-true- + " `, ) @@ -1462,8 +1462,8 @@ describe('Vapor Mode hydration', () => { expect(formatHtml(container.innerHTML)).toMatchInlineSnapshot( ` "
- -
" + + " `, ) @@ -1472,8 +1472,8 @@ describe('Vapor Mode hydration', () => { expect(formatHtml(container.innerHTML)).toMatchInlineSnapshot( ` "
-
true
-true- -
" + +
true
-true-" `, ) }) @@ -1496,8 +1496,8 @@ describe('Vapor Mode hydration', () => { expect(formatHtml(container.innerHTML)).toMatchInlineSnapshot( ` "
-
true
-true- -
" +
true
-true- + " `, ) @@ -1506,8 +1506,8 @@ describe('Vapor Mode hydration', () => { expect(formatHtml(container.innerHTML)).toMatchInlineSnapshot( ` "
- -
" + + " `, ) @@ -1515,8 +1515,8 @@ describe('Vapor Mode hydration', () => { await nextTick() expect(formatHtml(container.innerHTML)).toMatchInlineSnapshot(` "
-
true
-true- -
" + +
true
-true-" `) }) @@ -1539,9 +1539,10 @@ describe('Vapor Mode hydration', () => { expect(formatHtml(container.innerHTML)).toMatchInlineSnapshot( ` "
-
true
-true- -
true
-true- -
" +
true
-true- + +
true
-true- + " `, ) @@ -1550,9 +1551,10 @@ describe('Vapor Mode hydration', () => { expect(formatHtml(container.innerHTML)).toMatchInlineSnapshot( ` "
- - -
" + + + + " `, ) @@ -1560,9 +1562,10 @@ describe('Vapor Mode hydration', () => { await nextTick() expect(formatHtml(container.innerHTML)).toMatchInlineSnapshot(` "
-
true
-true- -
true
-true- -
" + +
true
-true- + +
true
-true-" `) }) @@ -1879,8 +1882,8 @@ describe('Vapor Mode hydration', () => {
foo
-bar-
foo
-bar- -
foo
-bar-
foo
-bar- - +
foo
-bar- +
foo
-bar- " `, ) diff --git a/packages/runtime-vapor/src/apiCreateFor.ts b/packages/runtime-vapor/src/apiCreateFor.ts index bdeccce2a5..38c60b9916 100644 --- a/packages/runtime-vapor/src/apiCreateFor.ts +++ b/packages/runtime-vapor/src/apiCreateFor.ts @@ -12,7 +12,11 @@ import { watch, } from '@vue/reactivity' import { isArray, isObject, isString } from '@vue/shared' -import { createComment, createTextNode } from './dom/node' +import { + createComment, + createTextNode, + updateLastLogicalChild, +} from './dom/node' import { type Block, insert, @@ -33,9 +37,8 @@ import { locateHydrationNode, setCurrentHydrationNode, } from './dom/hydration' -import { ForFragment, VaporFragment, findLastChild } from './fragment' +import { ForFragment, VaporFragment, findBlockNode } from './fragment' import { - type ChildItem, insertionAnchor, insertionParent, resetInsertionState, @@ -135,7 +138,7 @@ export const createFor = ( for (let i = 0; i < newLength; i++) { const nodes = mount(source, i).nodes if (isHydrating) { - setCurrentHydrationNode(findLastChild(nodes!)!.nextSibling) + setCurrentHydrationNode(findBlockNode(nodes!).nextNode) } } @@ -147,13 +150,9 @@ export const createFor = ( if (!parentAnchor || (parentAnchor && !isComment(parentAnchor, ']'))) { throw new Error(`v-for fragment anchor node was not found.`) } - // the lastLogicalChild is the fragment start anchor; replacing it with end anchor - // can avoid the call to locateEndAnchor within locateChildByLogicalIndex - if (_insertionParent && _insertionParent!.$llc) { - ;(parentAnchor as any as ChildItem).$idx = ( - _insertionParent!.$llc as ChildItem - ).$idx - _insertionParent.$llc = parentAnchor + + if (_insertionParent) { + updateLastLogicalChild(_insertionParent!, parentAnchor) } } } else { diff --git a/packages/runtime-vapor/src/block.ts b/packages/runtime-vapor/src/block.ts index 9dad78cb2f..765b017898 100644 --- a/packages/runtime-vapor/src/block.ts +++ b/packages/runtime-vapor/src/block.ts @@ -166,6 +166,17 @@ export function normalizeAnchor(node: Block): Node | undefined { } } +export function isFragmentBlock(block: Block): boolean { + if (isArray(block)) { + return true + } else if (isVaporComponent(block)) { + return isFragmentBlock(block.block!) + } else if (isFragment(block)) { + return isFragmentBlock(block.nodes) + } + return false +} + /** * dev / test only */ diff --git a/packages/runtime-vapor/src/componentSlots.ts b/packages/runtime-vapor/src/componentSlots.ts index 52ccf18cae..f01034426b 100644 --- a/packages/runtime-vapor/src/componentSlots.ts +++ b/packages/runtime-vapor/src/componentSlots.ts @@ -15,6 +15,7 @@ import { locateHydrationNode, } from './dom/hydration' import { DynamicFragment, type VaporFragment } from './fragment' +import { updateLastLogicalChild } from './dom/node' export type RawSlots = Record & { $?: DynamicSlotSource[] @@ -170,6 +171,9 @@ export function createSlot( if (fragment.insert) { ;(fragment as VaporFragment).hydrate!() } + if (_insertionParent) { + updateLastLogicalChild(_insertionParent!, fragment.anchor) + } if (_insertionAnchor !== undefined) { advanceHydrationNode(_insertionParent!) } diff --git a/packages/runtime-vapor/src/dom/hydration.ts b/packages/runtime-vapor/src/dom/hydration.ts index 8d4cba1e02..5ad6f1e307 100644 --- a/packages/runtime-vapor/src/dom/hydration.ts +++ b/packages/runtime-vapor/src/dom/hydration.ts @@ -58,6 +58,7 @@ function performHydration( ;(Node.prototype as any).$lpn = undefined ;(Node.prototype as any).$lan = undefined ;(Node.prototype as any).$lin = undefined + ;(Node.prototype as any).$curIdx = undefined isOptimized = true } @@ -186,6 +187,10 @@ function locateHydrationNodeImpl(): void { ? firstChild : locateChildByLogicalIndex(insertionParent!, insertionAnchor)! } + + insertionParent!.$llc = node + ;(node as ChildItem).$idx = insertionParent!.$curIdx = + insertionParent!.$curIdx === undefined ? 0 : insertionParent!.$curIdx + 1 } else { node = currentHydrationNode if (insertionParent && (!node || node.parentNode !== insertionParent)) { diff --git a/packages/runtime-vapor/src/dom/node.ts b/packages/runtime-vapor/src/dom/node.ts index 4bea538fbb..13500e2084 100644 --- a/packages/runtime-vapor/src/dom/node.ts +++ b/packages/runtime-vapor/src/dom/node.ts @@ -163,3 +163,14 @@ export function locateChildByLogicalIndex( return null } + +// use fragment end anchor as the logical child to avoid locateEndAnchor calls +// in locateChildByLogicalIndex +export function updateLastLogicalChild( + parent: InsertionParent, + child: Node, +): void { + if (!isComment(child, ']')) return + ;(child as any as ChildItem).$idx = parent.$curIdx || 0 + parent.$llc = child +} diff --git a/packages/runtime-vapor/src/fragment.ts b/packages/runtime-vapor/src/fragment.ts index 609e90b3f0..9a25e717ef 100644 --- a/packages/runtime-vapor/src/fragment.ts +++ b/packages/runtime-vapor/src/fragment.ts @@ -6,6 +6,7 @@ import { type TransitionOptions, type VaporTransitionHooks, insert, + isFragmentBlock, isValidBlock, remove, } from './block' @@ -178,14 +179,14 @@ export class DynamicFragment extends VaporFragment { } } + const { parentNode, nextNode } = findBlockNode(this.nodes)! // create an anchor - const { parentNode, nextSibling } = findLastChild(this)! queuePostFlushCb(() => { parentNode!.insertBefore( (this.anchor = __DEV__ ? createComment(this.anchorLabel!) : createTextNode()), - nextSibling, + nextNode, ) }) } @@ -243,7 +244,25 @@ function findInvalidFragment(fragment: VaporFragment): VaporFragment | null { : fragment } -export function findLastChild(node: Block): Node | undefined | null { +export function findBlockNode(block: Block): { + parentNode: Node | null + nextNode: Node | null +} { + let { parentNode, nextSibling: nextNode } = findLastChild(block)! + + // if nodes render as a fragment and the current nextNode is fragment + // end anchor, need to move to the next node + if (nextNode && isComment(nextNode, ']') && isFragmentBlock(block)) { + nextNode = nextNode.nextSibling + } + + return { + parentNode, + nextNode, + } +} + +function findLastChild(node: Block): Node | undefined | null { if (node && node instanceof Node) { return node } else if (isArray(node)) { diff --git a/packages/runtime-vapor/src/insertionState.ts b/packages/runtime-vapor/src/insertionState.ts index 9b993ba49e..a4bd23ed4c 100644 --- a/packages/runtime-vapor/src/insertionState.ts +++ b/packages/runtime-vapor/src/insertionState.ts @@ -16,6 +16,8 @@ export type InsertionParent = ParentNode & { $lpn?: Node | null // last append node $lan?: Node | null + // the logical index of current hydration node + $curIdx?: number } export let insertionParent: InsertionParent | undefined export let insertionAnchor: Node | 0 | undefined | null diff --git a/packages/runtime-vapor/src/vdomInterop.ts b/packages/runtime-vapor/src/vdomInterop.ts index 1e88ad00ab..9790cf282e 100644 --- a/packages/runtime-vapor/src/vdomInterop.ts +++ b/packages/runtime-vapor/src/vdomInterop.ts @@ -283,7 +283,7 @@ function createVDOMComponent( hydrateVNode(vnode, parentInstance as any) onScopeDispose(unmount, true) isMounted = true - frag.nodes = [vnode.el as Node] + frag.nodes = vnode.el as any } frag.insert = (parentNode, anchor, transition) => { @@ -316,7 +316,7 @@ function createVDOMComponent( } // update the fragment nodes - frag.nodes = [vnode.el as Node] + frag.nodes = vnode.el as any simpleSetCurrentInstance(prev) } @@ -441,6 +441,10 @@ function renderVDOMSlot( frag.hydrate = () => { render() + if (__DEV__ && isComment(currentHydrationNode!, ']')) { + throw new Error(`Failed to locate vdom slot anchor`) + } + frag.anchor = currentHydrationNode! isMounted = true }