From: daiwei Date: Fri, 19 Sep 2025 07:50:03 +0000 (+0800) Subject: refactor: reuse hydration state X-Git-Url: http://git.ipfire.org/gitweb.cgi?a=commitdiff_plain;h=80b8893e6a52f9e1bb0fcd32186f86930cb3e92e;p=thirdparty%2Fvuejs%2Fcore.git refactor: reuse hydration state --- diff --git a/packages/runtime-vapor/src/component.ts b/packages/runtime-vapor/src/component.ts index 9d4c5737f6..8d70322092 100644 --- a/packages/runtime-vapor/src/component.ts +++ b/packages/runtime-vapor/src/component.ts @@ -616,10 +616,8 @@ export function mountComponent( startMeasure(instance, `mount`) } if (instance.bm) invokeArrayFns(instance.bm) - if (!isHydrating) { - insert(instance.block, parent, anchor) - setComponentScopeId(instance) - } + insert(instance.block, parent, anchor) + if (!isHydrating) setComponentScopeId(instance) if (instance.m) queuePostFlushCb(() => invokeArrayFns(instance.m!)) instance.isMounted = true if (__DEV__) { diff --git a/packages/runtime-vapor/src/dom/hydration.ts b/packages/runtime-vapor/src/dom/hydration.ts index 9a4d923f74..704494bad3 100644 --- a/packages/runtime-vapor/src/dom/hydration.ts +++ b/packages/runtime-vapor/src/dom/hydration.ts @@ -1,7 +1,7 @@ import { warn } from '@vue/runtime-dom' import { type ChildItem, - getHydrationState, + incrementIndexOffset, insertionAnchor, insertionParent, resetInsertionState, @@ -30,9 +30,15 @@ function performHydration( // optimize anchor cache lookup ;(Comment.prototype as any).$fe = undefined ;(Node.prototype as any).$pns = undefined - ;(Node.prototype as any).$idx = undefined ;(Node.prototype as any).$uc = undefined + ;(Node.prototype as any).$idx = undefined ;(Node.prototype as any).$children = undefined + ;(Node.prototype as any).$idxMap = undefined + ;(Node.prototype as any).$prevDynamicCount = undefined + ;(Node.prototype as any).$anchorCount = undefined + ;(Node.prototype as any).$appendIndex = undefined + ;(Node.prototype as any).$indexOffset = undefined + isOptimized = true } enableHydrationNodeLookup() @@ -108,6 +114,7 @@ function adoptTemplateImpl(node: Node, template: string): Node | null { isComment(node.previousSibling!, '[') ) { node = node.parentNode!.insertBefore(createTextNode(' '), node) + incrementIndexOffset(node.parentNode!) break } } @@ -136,13 +143,19 @@ function adoptTemplateImpl(node: Node, template: string): Node | null { function locateHydrationNodeImpl(): void { let node: Node | null - if (insertionAnchor !== undefined) { - const hydrationState = getHydrationState(insertionParent!)! - const { prevDynamicCount, logicalChildren, appendAnchor } = hydrationState + let idxMap: number[] | undefined + if (insertionAnchor !== undefined && (idxMap = insertionParent!.$idxMap)) { + const { + $prevDynamicCount: prevDynamicCount = 0, + $appendIndex: appendIndex, + $indexOffset: indexOffset = 0, + $anchorCount: anchorCount = 0, + } = insertionParent! // prepend if (insertionAnchor === 0) { - // use prevDynamicCount as index to locate the hydration node - node = logicalChildren[prevDynamicCount] + // use prevDynamicCount as logical index to locate the hydration node + const realIndex = idxMap![prevDynamicCount] + indexOffset + node = insertionParent!.childNodes[realIndex] } // insert else if (insertionAnchor instanceof Node) { @@ -153,36 +166,42 @@ function locateHydrationNodeImpl(): void { // consecutive insert operations locate the correct hydration node. let { $idx, $uc: usedCount } = insertionAnchor as ChildItem if (usedCount !== undefined) { - node = logicalChildren[$idx + usedCount + 1] + const realIndex = idxMap![$idx + usedCount + 1] + indexOffset + node = insertionParent!.childNodes[realIndex] usedCount++ } else { node = insertionAnchor // first use of this anchor: it doesn't consume the next child // so we track unique anchor appearances for later offset correction - hydrationState.uniqueAnchorCount++ + insertionParent!.$anchorCount = anchorCount + 1 usedCount = 0 } ;(insertionAnchor as ChildItem).$uc = usedCount } // append else { - if (appendAnchor) { - node = logicalChildren[(appendAnchor as ChildItem).$idx + 1] + let realIndex: number + if (appendIndex !== null && appendIndex !== undefined) { + realIndex = idxMap![appendIndex + 1] + indexOffset + node = insertionParent!.childNodes[realIndex] } else { - node = + if (insertionAnchor === null) { // insertionAnchor is null, indicates no previous static nodes // use the first child as hydration node - insertionAnchor === null - ? logicalChildren[0] - : // insertionAnchor is a number > 0 - // indicates how many static nodes precede the node to append - // use it as index to locate the hydration node - logicalChildren[prevDynamicCount + insertionAnchor] + realIndex = idxMap![0] + indexOffset + node = insertionParent!.childNodes[realIndex] + } else { + // insertionAnchor is a number > 0 + // indicates how many static nodes precede the node to append + // use it as index to locate the hydration node + realIndex = idxMap![prevDynamicCount + insertionAnchor] + indexOffset + node = insertionParent!.childNodes[realIndex] + } } - hydrationState.appendAnchor = node + insertionParent!.$appendIndex = (node as ChildItem).$idx } - hydrationState.prevDynamicCount++ + insertionParent!.$prevDynamicCount = prevDynamicCount + 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 a48ecf5bbb..df291e40e7 100644 --- a/packages/runtime-vapor/src/dom/node.ts +++ b/packages/runtime-vapor/src/dom/node.ts @@ -1,10 +1,6 @@ /* @__NO_SIDE_EFFECTS__ */ -import { - type ChildItem, - type InsertionParent, - getHydrationState, -} from '../insertionState' +import type { ChildItem, InsertionParent } from '../insertionState' export function createElement(tagName: string): HTMLElement { return document.createElement(tagName) @@ -70,21 +66,27 @@ export function _nthChild(node: InsertionParent, i: number): Node { */ /* @__NO_SIDE_EFFECTS__ */ export function __nthChild(node: Node, i: number): Node { - const hydrationState = getHydrationState(node as ParentNode) - if (hydrationState) { - const { prevDynamicCount, uniqueAnchorCount, logicalChildren } = - hydrationState + const parent = node as InsertionParent + if (parent.$idxMap) { + const { + $prevDynamicCount: prevDynamicCount = 0, + $anchorCount: anchorCount = 0, + $idxMap: idxMap, + $indexOffset: indexOffset = 0, + } = parent // prevDynamicCount tracks how many dynamic nodes have been processed // so far (prepend/insert/append). // For anchor-based insert, the first time an anchor is used we adopt the - // anchor node itself and do NOT consume the next child in `logicalChildren`, + // anchor node itself and do NOT consume the next child in `idxMap`, // yet prevDynamicCount is still incremented. This overcounts the base // offset by 1 per unique anchor that has appeared. - // uniqueAnchorCount equals the number of unique anchors seen, so we + // anchorCount equals the number of unique anchors seen, so we // subtract it to neutralize those "first-use doesn't consume" cases: - // base = prevDynamicCount - uniqueAnchorCount - // Then index from this base: logicalChildren[base + i]. - return logicalChildren[prevDynamicCount - uniqueAnchorCount + i] + // base = prevDynamicCount - anchorCount + // Then index from this base: idxMap[base + i] + indexOffset. + const logicalIndex = prevDynamicCount - anchorCount + i + const realIndex = idxMap[logicalIndex] + indexOffset + return node.childNodes[realIndex] } return node.childNodes[i] } @@ -100,11 +102,13 @@ export function _next(node: Node): Node { */ /* @__NO_SIDE_EFFECTS__ */ export function __next(node: Node): Node { - const hydrationState = getHydrationState(node.parentNode!) - if (hydrationState) { - const { logicalChildren } = hydrationState + const parent = node.parentNode! as InsertionParent + if (parent.$idxMap) { + const { $idxMap: idxMap, $indexOffset: indexOffset = 0 } = parent const { $idx, $uc: usedCount = 0 } = node as ChildItem - return logicalChildren[$idx + usedCount + 1] + const logicalIndex = $idx + usedCount + 1 + const realIndex = idxMap[logicalIndex] + indexOffset + return node.parentNode!.childNodes[realIndex] } return node.nextSibling! } diff --git a/packages/runtime-vapor/src/dom/prop.ts b/packages/runtime-vapor/src/dom/prop.ts index ccc941e299..d9a38fc39d 100644 --- a/packages/runtime-vapor/src/dom/prop.ts +++ b/packages/runtime-vapor/src/dom/prop.ts @@ -275,6 +275,8 @@ export function optimizePropertyLookup(): void { proto.$transition = undefined proto.$key = undefined proto.$evtclick = undefined + proto.$children = undefined + proto.$idx = undefined proto.$root = false proto.$html = proto.$txt = diff --git a/packages/runtime-vapor/src/dom/template.ts b/packages/runtime-vapor/src/dom/template.ts index e2acbc96e6..9c4b3ca1d3 100644 --- a/packages/runtime-vapor/src/dom/template.ts +++ b/packages/runtime-vapor/src/dom/template.ts @@ -3,11 +3,22 @@ import { child, createElement, createTextNode } from './node' let t: HTMLTemplateElement +export let currentTemplateFn: (Function & { $idxMap?: number[] }) | undefined = + undefined + +export function resetTemplateFn(): void { + currentTemplateFn = undefined +} + /*! #__NO_SIDE_EFFECTS__ */ -export function template(html: string, root?: boolean) { +export function template( + html: string, + root?: boolean, +): () => Node & { $root?: true } { let node: Node - return (): Node & { $root?: true } => { + const fn = () => { if (isHydrating) { + currentTemplateFn = fn if (__DEV__ && !currentHydrationNode) { // TODO this should not happen throw new Error('No current hydration node') @@ -32,4 +43,5 @@ export function template(html: string, root?: boolean) { if (root) (ret as any).$root = true return ret } + return fn } diff --git a/packages/runtime-vapor/src/fragment.ts b/packages/runtime-vapor/src/fragment.ts index 58f9d93799..b13458cfdf 100644 --- a/packages/runtime-vapor/src/fragment.ts +++ b/packages/runtime-vapor/src/fragment.ts @@ -24,6 +24,7 @@ import { } from './components/Transition' import { type VaporComponentInstance, isVaporComponent } from './component' import { isArray } from '@vue/shared' +import { incrementIndexOffset } from './insertionState' export class VaporFragment implements TransitionOptions @@ -185,6 +186,8 @@ export class DynamicFragment extends VaporFragment { (this.anchor = createComment(this.anchorLabel!)), nextSibling, ) + // increment index offset since we dynamically inserted a comment node + incrementIndexOffset(parentNode!) advanceHydrationNode(this.anchor) } } diff --git a/packages/runtime-vapor/src/insertionState.ts b/packages/runtime-vapor/src/insertionState.ts index baf9854a4e..f1e128d42e 100644 --- a/packages/runtime-vapor/src/insertionState.ts +++ b/packages/runtime-vapor/src/insertionState.ts @@ -1,22 +1,27 @@ import { isComment, isHydrating } from './dom/hydration' +import { currentTemplateFn, resetTemplateFn } from './dom/template' export type ChildItem = ChildNode & { $idx: number // used count as an anchor $uc?: number } -export type InsertionParent = ParentNode & { $children?: ChildItem[] } -type HydrationState = { - // static nodes and the start anchors of fragments - logicalChildren: ChildItem[] + +export type InsertionParent = ParentNode & { + $children?: ChildItem[] + /** + * hydration-specific properties + */ + // mapping from logical index to real index in childNodes + $idxMap?: number[] // hydrated dynamic children count so far - prevDynamicCount: number + $prevDynamicCount?: number // number of unique insertion anchors that have appeared - uniqueAnchorCount: number - // current append anchor - appendAnchor: Node | null + $anchorCount?: number + // last append index + $appendIndex?: number | null + // number of dynamically inserted nodes (e.g., comment anchors) + $indexOffset?: number } - -const hydrationStateCache = new WeakMap() export let insertionParent: InsertionParent | undefined export let insertionAnchor: Node | 0 | undefined | null @@ -35,6 +40,7 @@ export function setInsertionState( if (isHydrating) { insertionAnchor = anchor as Node initializeHydrationState(parent) + resetTemplateFn() } else { // special handling append anchor value to null insertionAnchor = @@ -46,12 +52,13 @@ export function setInsertionState( } } -function initializeHydrationState(parent: ParentNode) { - if (!hydrationStateCache.has(parent)) { +function initializeHydrationState(parent: InsertionParent) { + if (!parent.$idxMap) { const childNodes = parent.childNodes const len = childNodes.length - // fast path for single child case. No need to build logicalChildren + // fast path for single child case. use first child as hydration node + // no need to build logical index map if ( len === 1 || (len === 3 && @@ -62,48 +69,65 @@ function initializeHydrationState(parent: ParentNode) { return } - const logicalChildren = new Array(len) as ChildItem[] - // Build logical children: - // - static node: keep the node as a child - // - fragment: keep only the start anchor ('') as a child - let index = 0 - for (let i = 0; i < len; i++) { - const n = childNodes[i] as ChildItem - n.$idx = index - if (n.nodeType === 8) { - const data = (n as any as Comment).data - // vdom fragment - if (data === '[') { - logicalChildren[index++] = n - // find matching end anchor, accounting for nested fragments - let depth = 1 - let j = i + 1 - for (; j < len; j++) { - const c = childNodes[j] as Comment - if (c.nodeType === 8) { - const d = c.data - if (d === '[') depth++ - else if (d === ']') { - depth-- - if (depth === 0) break - } + if (currentTemplateFn) { + if (currentTemplateFn.$idxMap) { + const idxMap = (parent.$idxMap = currentTemplateFn.$idxMap) + // set $idx to childNodes + for (let i = 0; i < idxMap.length; i++) { + ;(childNodes[idxMap[i]] as ChildItem).$idx = i + } + } else { + parent.$idxMap = currentTemplateFn.$idxMap = buildLogicalIndexMap( + len, + childNodes, + ) + } + } else { + parent.$idxMap = buildLogicalIndexMap(len, childNodes) + } + parent.$prevDynamicCount = 0 + parent.$anchorCount = 0 + parent.$appendIndex = null + parent.$indexOffset = 0 + } +} + +function buildLogicalIndexMap(len: number, childNodes: NodeListOf) { + const idxMap = new Array() as number[] + // Build logical index map: + // - static node: map logical index to real index + // - fragment: map logical index to start anchor's real index + let logicalIndex = 0 + for (let i = 0; i < len; i++) { + const n = childNodes[i] as ChildItem + n.$idx = logicalIndex + if (n.nodeType === 8) { + const data = (n as any as Comment).data + // vdom fragment + if (data === '[') { + idxMap[logicalIndex++] = i + // find matching end anchor, accounting for nested fragments + let depth = 1 + let j = i + 1 + for (; j < len; j++) { + const c = childNodes[j] as Comment + if (c.nodeType === 8) { + const d = c.data + if (d === '[') depth++ + else if (d === ']') { + depth-- + if (depth === 0) break } } - // jump i to the end anchor - i = j - continue } + // jump i to the end anchor + i = j + continue } - logicalChildren[index++] = n } - logicalChildren.length = index - hydrationStateCache.set(parent, { - logicalChildren, - prevDynamicCount: 0, - uniqueAnchorCount: 0, - appendAnchor: null, - }) + idxMap[logicalIndex++] = i } + return idxMap } function cacheTemplateChildren(parent: InsertionParent) { @@ -124,8 +148,8 @@ export function resetInsertionState(): void { insertionParent = insertionAnchor = undefined } -export function getHydrationState( - parent: ParentNode, -): HydrationState | undefined { - return hydrationStateCache.get(parent) +export function incrementIndexOffset(parent: InsertionParent): void { + if (parent.$indexOffset !== undefined) { + parent.$indexOffset++ + } }