refactor: update logical child handling and optimize hydration state management

This commit is contained in:
daiwei 2025-09-30 09:50:11 +08:00
parent 4bc9c8b85c
commit 679a67dbba
8 changed files with 50 additions and 89 deletions

View File

@ -179,8 +179,8 @@ function genInsertionState(
: anchor === -1 // -1 indicates prepend : anchor === -1 // -1 indicates prepend
? `0` // runtime anchor value for prepend ? `0` // runtime anchor value for prepend
: append // -2 indicates append : append // -2 indicates append
? // null or number > 0 for append ? // null or anchor > 0 for append
// number > 0 is used for locate the previous static node during hydration // anchor > 0 is the logical index of append node - used for locate node during hydration
anchor === 0 anchor === 0
? 'null' ? 'null'
: `${anchor}` : `${anchor}`

View File

@ -60,6 +60,7 @@ export const transformChildren: NodeTransform = (node, context) => {
function processDynamicChildren(context: TransformContext<ElementNode>) { function processDynamicChildren(context: TransformContext<ElementNode>) {
let prevDynamics: IRDynamicInfo[] = [] let prevDynamics: IRDynamicInfo[] = []
let staticCount = 0 let staticCount = 0
let dynamicCount = 0
const children = context.dynamic.children const children = context.dynamic.children
for (const [index, child] of children.entries()) { for (const [index, child] of children.entries()) {
@ -77,6 +78,7 @@ function processDynamicChildren(context: TransformContext<ElementNode>) {
} else { } else {
registerInsertion(prevDynamics, context, -1 /* prepend */) registerInsertion(prevDynamics, context, -1 /* prepend */)
} }
dynamicCount += prevDynamics.length
prevDynamics = [] prevDynamics = []
} }
staticCount++ staticCount++
@ -84,7 +86,7 @@ function processDynamicChildren(context: TransformContext<ElementNode>) {
} }
if (prevDynamics.length) { if (prevDynamics.length) {
registerInsertion(prevDynamics, context, staticCount, true) registerInsertion(prevDynamics, context, dynamicCount + staticCount, true)
} }
} }

View File

@ -151,14 +151,13 @@ export const createFor = (
if (!parentAnchor || (parentAnchor && !isComment(parentAnchor, ']'))) { if (!parentAnchor || (parentAnchor && !isComment(parentAnchor, ']'))) {
throw new Error(`v-for fragment anchor node was not found.`) throw new Error(`v-for fragment anchor node was not found.`)
} }
// the lastLogicalChild is the fragment start anchor; replacing it with end anchor
// $lastLogicalChild is the fragment start anchor; replacing it with end anchor
// can avoid the call to locateEndAnchor within locateChildByLogicalIndex // can avoid the call to locateEndAnchor within locateChildByLogicalIndex
if (_insertionParent && _insertionParent!.$lastLogicalChild) { if (_insertionParent && _insertionParent!.$llc) {
;(parentAnchor as any as ChildItem).$idx = ( ;(parentAnchor as any as ChildItem).$idx = (
_insertionParent!.$lastLogicalChild as ChildItem _insertionParent!.$llc as ChildItem
).$idx ).$idx
_insertionParent.$lastLogicalChild = parentAnchor _insertionParent.$llc = parentAnchor
} }
} }
} else { } else {

View File

@ -69,11 +69,11 @@ export function isValidBlock(block: Block): boolean {
export function insert( export function insert(
block: Block, block: Block,
parent: ParentNode & { $prependAnchor?: Node | null }, parent: ParentNode & { $fc?: Node | null },
anchor: Node | null | 0 = null, // 0 means prepend anchor: Node | null | 0 = null, // 0 means prepend
parentSuspense?: any, // TODO Suspense parentSuspense?: any, // TODO Suspense
): void { ): void {
anchor = anchor === 0 ? parent.$prependAnchor || _child(parent) : anchor anchor = anchor === 0 ? parent.$fc || _child(parent) : anchor
if (block instanceof Node) { if (block instanceof Node) {
if (!isHydrating) { if (!isHydrating) {
// only apply transition on Element nodes // only apply transition on Element nodes

View File

@ -44,11 +44,11 @@ function performHydration<T>(
// optimize anchor cache lookup // optimize anchor cache lookup
;(Comment.prototype as any).$fe = undefined ;(Comment.prototype as any).$fe = undefined
;(Node.prototype as any).$pns = undefined ;(Node.prototype as any).$pns = undefined
;(Node.prototype as any).$uc = undefined
;(Node.prototype as any).$idx = undefined ;(Node.prototype as any).$idx = undefined
;(Node.prototype as any).$prevDynamicCount = undefined ;(Node.prototype as any).$llc = undefined
;(Node.prototype as any).$anchorCount = undefined ;(Node.prototype as any).$lpn = undefined
;(Node.prototype as any).$appendIndex = undefined ;(Node.prototype as any).$lan = undefined
;(Node.prototype as any).$lin = undefined
isOptimized = true isOptimized = true
} }
@ -146,67 +146,37 @@ function adoptTemplateImpl(node: Node, template: string): Node | null {
return node return node
} }
function nextNode(node: Node): Node | null {
return isComment(node, '[')
? locateEndAnchor(node as Anchor)!.nextSibling
: node.nextSibling
}
function locateHydrationNodeImpl(): void { function locateHydrationNodeImpl(): void {
let node: Node | null let node: Node | null
if (insertionAnchor !== undefined) { if (insertionAnchor !== undefined) {
const { const { $lpn: lastPrepend, $lan: lastAppend, firstChild } = insertionParent!
$prevDynamicCount: prevDynamicCount = 0,
$appendIndex: appendIndex,
$anchorCount: anchorCount = 0,
} = insertionParent!
// prepend // prepend
if (insertionAnchor === 0) { if (insertionAnchor === 0) {
// use prevDynamicCount as logical index to locate the hydration node node = insertionParent!.$lpn = lastPrepend
node = ? nextNode(lastPrepend)
prevDynamicCount === 0 && : firstChild
currentHydrationNode!.parentNode === insertionParent
? currentHydrationNode
: locateChildByLogicalIndex(insertionParent!, prevDynamicCount)!
} }
// insert // insert
else if (insertionAnchor instanceof Node) { else if (insertionAnchor instanceof Node) {
// handling insertion anchors: const { $lin: lastInsertedNode } = insertionAnchor as ChildItem
// 1. first encounter: use insertionAnchor itself as the hydration node node = (insertionAnchor as ChildItem).$lin = lastInsertedNode
// 2. subsequent: use node following the insertionAnchor as the hydration node ? nextNode(lastInsertedNode)
// used count tracks how many times insertionAnchor has been used, ensuring : insertionAnchor
// consecutive insert operations locate the correct hydration node.
let { $idx, $uc: usedCount } = insertionAnchor as ChildItem
if (usedCount !== undefined) {
node = locateChildByLogicalIndex(
insertionParent!,
$idx + usedCount + 1,
)!
usedCount++
} else {
insertionParent!.$lastLogicalChild = node = insertionAnchor
// first use of this anchor: it doesn't consume the next child
// so we track unique anchor appearances for later offset correction
insertionParent!.$anchorCount = anchorCount + 1
usedCount = 0
}
;(insertionAnchor as ChildItem).$uc = usedCount
} }
// append // append
else { else {
if (appendIndex !== null && appendIndex !== undefined) { node = insertionParent!.$lan = lastAppend
node = locateChildByLogicalIndex(insertionParent!, appendIndex + 1)! ? nextNode(lastAppend)
} else { : insertionAnchor === null
if (insertionAnchor === null) { ? firstChild
node = : locateChildByLogicalIndex(insertionParent!, insertionAnchor)!
currentHydrationNode!.parentNode === insertionParent
? currentHydrationNode
: locateChildByLogicalIndex(insertionParent!, 0)!
} else {
node = locateChildByLogicalIndex(
insertionParent!,
prevDynamicCount + insertionAnchor,
)!
}
}
insertionParent!.$appendIndex = (node as ChildItem).$idx
} }
insertionParent!.$prevDynamicCount = prevDynamicCount + 1
} else { } else {
node = currentHydrationNode node = currentHydrationNode
if (insertionParent && (!node || node.parentNode !== insertionParent)) { if (insertionParent && (!node || node.parentNode !== insertionParent)) {

View File

@ -142,13 +142,13 @@ export function locateChildByLogicalIndex(
parent: InsertionParent, parent: InsertionParent,
logicalIndex: number, logicalIndex: number,
): Node | null { ): Node | null {
let child = (parent.$lastLogicalChild || parent.firstChild) as ChildItem let child = (parent.$llc || parent.firstChild) as ChildItem
let fromIndex = child.$idx || 0 let fromIndex = child.$idx || 0
while (child) { while (child) {
if (fromIndex === logicalIndex) { if (fromIndex === logicalIndex) {
child.$idx = logicalIndex child.$idx = logicalIndex
return (parent.$lastLogicalChild = child) return (parent.$llc = child)
} }
child = ( child = (

View File

@ -386,8 +386,7 @@ export function optimizePropertyLookup(): void {
const proto = Element.prototype as any const proto = Element.prototype as any
proto.$transition = undefined proto.$transition = undefined
proto.$key = undefined proto.$key = undefined
proto.$prependAnchor = proto.$evtclick = undefined proto.$fc = proto.$evtclick = undefined
proto.$idx = undefined
proto.$root = false proto.$root = false
proto.$html = proto.$html =
proto.$txt = proto.$txt =

View File

@ -1,24 +1,21 @@
import { isHydrating } from './dom/hydration' import { isHydrating } from './dom/hydration'
export type ChildItem = ChildNode & { export type ChildItem = ChildNode & {
// logical index, used during hydration to locate the node
$idx: number $idx: number
// used count as an anchor // last inserted node
$uc?: number $lin?: Node | null
} }
export type InsertionParent = ParentNode & { export type InsertionParent = ParentNode & {
$prependAnchor?: Node | null // cache the first child for potential consecutive prepends
$fc?: Node | null
/**
* hydration-specific properties
*/
// hydrated dynamic children count so far
$prevDynamicCount?: number
// number of unique insertion anchors that have appeared
$anchorCount?: number
// last append index
$appendIndex?: number | null
// last located logical child // last located logical child
$lastLogicalChild?: Node | null $llc?: Node | null
// last prepend node
$lpn?: Node | null
// last append node
$lan?: Node | null
} }
export let insertionParent: InsertionParent | undefined export let insertionParent: InsertionParent | undefined
export let insertionAnchor: Node | 0 | undefined | null export let insertionAnchor: Node | 0 | undefined | null
@ -29,7 +26,7 @@ export let insertionAnchor: Node | 0 | undefined | null
* insertion on client-side render, and used for node adoption during hydration. * insertion on client-side render, and used for node adoption during hydration.
*/ */
export function setInsertionState( export function setInsertionState(
parent: ParentNode & { $prependAnchor?: Node | null }, parent: ParentNode & { $fc?: Node | null },
anchor?: Node | 0 | null | number, anchor?: Node | 0 | null | number,
): void { ): void {
insertionParent = parent insertionParent = parent
@ -37,19 +34,13 @@ export function setInsertionState(
if (anchor !== undefined) { if (anchor !== undefined) {
if (isHydrating) { if (isHydrating) {
insertionAnchor = anchor as Node insertionAnchor = anchor as Node
// when the setInsertionState is called for the first time, reset $lastLogicalChild,
// in order to reuse it in locateChildByLogicalIndex
if (insertionParent.$prevDynamicCount === undefined) {
insertionParent!.$lastLogicalChild = null
}
} else { } else {
// special handling append anchor value to null // special handling append anchor value to null
insertionAnchor = insertionAnchor =
typeof anchor === 'number' && anchor > 0 ? null : (anchor as Node) typeof anchor === 'number' && anchor > 0 ? null : (anchor as Node)
// track the first child for potential future use if (anchor === 0 && !parent.$fc) {
if (anchor === 0 && !parent.$prependAnchor) { parent.$fc = parent.firstChild
parent.$prependAnchor = parent.firstChild
} }
} }
} else { } else {