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 (prevDynamics.length) {
if (hasStaticTemplate) {
context.childrenTemplate[index - prevDynamics.length] = `<!>`
prevDynamics[0].flags -= DynamicFlag.NON_TEMPLATE
const anchor = (prevDynamics[0].anchor = context.increaseId())
registerInsertion(prevDynamics, context, anchor)
// each dynamic child gets its own placeholder node.
// this makes it easier to locate the corresponding node during hydration.
for (let i = 0; i < prevDynamics.length; i++) {
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 {
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(
`<template>
<div>
@ -333,11 +333,15 @@ describe('Vapor Mode hydration', () => {
Child: `<template>{{ data }}</template>`,
},
)
expect(container.innerHTML).toMatchInlineSnapshot(``)
expect(container.innerHTML).toMatchInlineSnapshot(
`"<div><span></span><!--[[-->foo<!--]]--><!--[[--> <!--]]--><!--[[--> foo <!--]]--><!--[[--> <!--]]--><!--[[-->foo<!--]]--><span></span></div>"`,
)
data.value = 'bar'
await nextTick()
expect(container.innerHTML).toMatchInlineSnapshot(``)
expect(container.innerHTML).toMatchInlineSnapshot(
`"<div><span></span><!--[[-->bar<!--]]--><!--[[--> <!--]]--><!--[[--> bar <!--]]--><!--[[--> <!--]]--><!--[[-->bar<!--]]--><span></span></div>"`,
)
})
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 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
/**
@ -76,16 +76,8 @@ function locateHydrationNodeImpl() {
if (insertionAnchor === 0) {
node = child(insertionParent!)
} else if (insertionAnchor) {
// dynamic anchor `<!--[[-->`
if (isDynamicStart(insertionAnchor)) {
const anchor = (insertionParent!.$lds = insertionParent!.$lds
? // continuous dynamic children, the next dynamic start must exist
locateNextDynamicStart(insertionParent!.$lds)!
: insertionAnchor)
node = anchor.nextSibling
} else {
// for dynamic children, use insertionAnchor as the node
node = insertionAnchor
}
} else {
node = insertionParent ? insertionParent.lastChild : currentHydrationNode
if (node && isComment(node, ']')) {
@ -127,32 +119,3 @@ function locateHydrationNodeImpl() {
resetInsertionState()
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__ */
import { isComment, isHydrating } from './hydration'
export function createTextNode(value = ''): Text {
return document.createTextNode(value)
}
@ -25,5 +28,20 @@ export function nthChild(node: Node, i: number): Node {
/*! #__NO_SIDE_EFFECTS__ */
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()
}