refactor: skip dynamic anchors and empty text nodes

This commit is contained in:
daiwei 2025-04-23 10:53:57 +08:00
parent d8443d3754
commit 3108d91dfa
4 changed files with 46 additions and 48 deletions

View File

@ -70,10 +70,23 @@ function processDynamicChildren(context: TransformContext<ElementNode>) {
if (!(child.flags & DynamicFlag.NON_TEMPLATE)) { if (!(child.flags & DynamicFlag.NON_TEMPLATE)) {
if (prevDynamics.length) { if (prevDynamics.length) {
if (hasStaticTemplate) { if (hasStaticTemplate) {
context.childrenTemplate[index - prevDynamics.length] = `<!>` // each dynamic child gets its own placeholder node.
prevDynamics[0].flags -= DynamicFlag.NON_TEMPLATE // this makes it easier to locate the corresponding node during hydration.
const anchor = (prevDynamics[0].anchor = context.increaseId()) for (let i = 0; i < prevDynamics.length; i++) {
registerInsertion(prevDynamics, context, anchor) const idx = index - prevDynamics.length + i
context.childrenTemplate[idx] = `<!>`
const dynamicChild = prevDynamics[i]
dynamicChild.flags -= DynamicFlag.NON_TEMPLATE
const anchor = (dynamicChild.anchor = context.increaseId())
if (
dynamicChild.operation &&
isBlockOperation(dynamicChild.operation)
) {
// block types
dynamicChild.operation.parent = context.reference()
dynamicChild.operation.anchor = anchor
}
}
} else { } else {
registerInsertion(prevDynamics, context, -1 /* prepend */) registerInsertion(prevDynamics, context, -1 /* prepend */)
} }

View File

@ -317,7 +317,7 @@ describe('Vapor Mode hydration', () => {
) )
}) })
test.todo('mixed component and text with anchor insertion', async () => { test('mixed component and text with anchor insertion', async () => {
const { container, data } = await testHydration( const { container, data } = await testHydration(
`<template> `<template>
<div> <div>
@ -333,11 +333,15 @@ describe('Vapor Mode hydration', () => {
Child: `<template>{{ data }}</template>`, Child: `<template>{{ data }}</template>`,
}, },
) )
expect(container.innerHTML).toMatchInlineSnapshot(``) expect(container.innerHTML).toMatchInlineSnapshot(
`"<div><span></span><!--[[-->foo<!--]]--><!--[[--> <!--]]--><!--[[--> foo <!--]]--><!--[[--> <!--]]--><!--[[-->foo<!--]]--><span></span></div>"`,
)
data.value = 'bar' data.value = 'bar'
await nextTick() await nextTick()
expect(container.innerHTML).toMatchInlineSnapshot(``) expect(container.innerHTML).toMatchInlineSnapshot(
`"<div><span></span><!--[[-->bar<!--]]--><!--[[--> <!--]]--><!--[[--> bar <!--]]--><!--[[--> <!--]]--><!--[[-->bar<!--]]--><span></span></div>"`,
)
}) })
test.todo('if') test.todo('if')

View File

@ -37,7 +37,7 @@ export function withHydration(container: ParentNode, fn: () => void): void {
export let adoptTemplate: (node: Node, template: string) => Node | null export let adoptTemplate: (node: Node, template: string) => Node | null
export let locateHydrationNode: () => void export let locateHydrationNode: () => void
const isComment = (node: Node, data: string): node is Anchor => export const isComment = (node: Node, data: string): node is Anchor =>
node.nodeType === 8 && (node as Comment).data === data node.nodeType === 8 && (node as Comment).data === data
/** /**
@ -76,16 +76,8 @@ function locateHydrationNodeImpl() {
if (insertionAnchor === 0) { if (insertionAnchor === 0) {
node = child(insertionParent!) node = child(insertionParent!)
} else if (insertionAnchor) { } else if (insertionAnchor) {
// dynamic anchor `<!--[[-->` // for dynamic children, use insertionAnchor as the node
if (isDynamicStart(insertionAnchor)) { node = insertionAnchor
const anchor = (insertionParent!.$lds = insertionParent!.$lds
? // continuous dynamic children, the next dynamic start must exist
locateNextDynamicStart(insertionParent!.$lds)!
: insertionAnchor)
node = anchor.nextSibling
} else {
node = insertionAnchor
}
} else { } else {
node = insertionParent ? insertionParent.lastChild : currentHydrationNode node = insertionParent ? insertionParent.lastChild : currentHydrationNode
if (node && isComment(node, ']')) { if (node && isComment(node, ']')) {
@ -127,32 +119,3 @@ function locateHydrationNodeImpl() {
resetInsertionState() resetInsertionState()
currentHydrationNode = node currentHydrationNode = node
} }
function isDynamicStart(node: Node): node is Anchor {
return isComment(node, '[[')
}
function locateNextDynamicStart(anchor: Anchor): Anchor | undefined {
let cur: Node | null = anchor
let end = null
let depth = 0
while (cur) {
cur = cur.nextSibling
if (cur) {
if (isComment(cur, '[[')) {
depth++
} else if (isComment(cur, ']]')) {
if (!depth) {
end = cur
break
} else {
depth--
}
}
}
}
if (end) {
return end!.nextSibling as Anchor
}
}

View File

@ -1,4 +1,7 @@
/*! #__NO_SIDE_EFFECTS__ */ /*! #__NO_SIDE_EFFECTS__ */
import { isComment, isHydrating } from './hydration'
export function createTextNode(value = ''): Text { export function createTextNode(value = ''): Text {
return document.createTextNode(value) return document.createTextNode(value)
} }
@ -25,5 +28,20 @@ export function nthChild(node: Node, i: number): Node {
/*! #__NO_SIDE_EFFECTS__ */ /*! #__NO_SIDE_EFFECTS__ */
export function next(node: Node): Node { export function next(node: Node): Node {
return node.nextSibling! let n = node.nextSibling!
if (isHydrating) {
// skip dynamic anchors and empty text nodes
while (n && (isDynamicAnchor(n) || isEmptyText(n))) {
n = n.nextSibling!
}
}
return n
}
function isDynamicAnchor(node: Node): node is Comment {
return isComment(node, '[[') || isComment(node, ']]')
}
function isEmptyText(node: Node): node is Text {
return node.nodeType === 3 && !(node as Text).data.trim()
} }