wip: refactor

This commit is contained in:
daiwei 2025-04-23 21:45:10 +08:00
parent 3e7f093519
commit 04eadd859a
5 changed files with 165 additions and 76 deletions

View File

@ -398,7 +398,7 @@ describe('ssr: element', () => {
})
describe('dynamic anchor', () => {
test('consecutive components', () => {
test('two consecutive components', () => {
expect(
getCompiledString(`
<div>
@ -409,12 +409,37 @@ describe('ssr: element', () => {
</div>
`),
).toMatchInlineSnapshot(`
"\`<div><div></div><!--[[-->\`)
"\`<div><div></div>\`)
_push(_ssrRenderComponent(_component_Comp1, null, null, _parent))
_push(\`<!--]]--><!--[[-->\`)
_push(\`<!--[[-->\`)
_push(_ssrRenderComponent(_component_Comp2, null, null, _parent))
_push(\`<!--]]--><div></div></div>\`"
`)
})
test('multiple consecutive components', () => {
expect(
getCompiledString(`
<div>
<div/>
<Comp1/>
<Comp2/>
<Comp3/>
<Comp4/>
<div/>
</div>
`),
).toMatchInlineSnapshot(`
"\`<div><div></div>\`)
_push(_ssrRenderComponent(_component_Comp1, null, null, _parent))
_push(\`<!--[[-->\`)
_push(_ssrRenderComponent(_component_Comp2, null, null, _parent))
_push(\`<!--]]--><!--[[-->\`)
_push(_ssrRenderComponent(_component_Comp3, null, null, _parent))
_push(\`<!--]]-->\`)
_push(_ssrRenderComponent(_component_Comp4, null, null, _parent))
_push(\`<div></div></div>\`"
`)
})
})
})

View File

@ -7,6 +7,7 @@ import {
type IfStatement,
type JSChildNode,
NodeTypes,
type PlainElementNode,
type RootNode,
type TemplateChildNode,
type TemplateLiteral,
@ -166,10 +167,14 @@ export function processChildren(
context.pushStringPart(`<!--[-->`)
}
const { children } = parent
const { children, type, tagType } = parent as PlainElementNode
const inElement =
type === NodeTypes.ELEMENT && tagType === ElementTypes.ELEMENT
if (inElement) processChildrenDynamicInfo(children)
for (let i = 0; i < children.length; i++) {
const child = children[i]
if (shouldProcessAsDynamic(parent, child)) {
if (inElement && shouldProcessChildAsDynamic(parent, child)) {
processChildren(
{ children: [child] },
context,
@ -274,87 +279,127 @@ const isStaticChildNode = (c: TemplateChildNode): boolean =>
c.type === NodeTypes.TEXT ||
c.type === NodeTypes.COMMENT
interface DynamicInfo {
hasStaticPrevious: boolean
hasStaticNext: boolean
prevDynamicCount: number
nextDynamicCount: number
}
function processChildrenDynamicInfo(
children: (TemplateChildNode & { _ssrDynamicInfo?: DynamicInfo })[],
): void {
const filteredChildren = children.filter(
child => !(child.type === NodeTypes.TEXT && !child.content.trim()),
)
for (let i = 0; i < filteredChildren.length; i++) {
const child = filteredChildren[i]
if (isStaticChildNode(child)) continue
child._ssrDynamicInfo = {
hasStaticPrevious: false,
hasStaticNext: false,
prevDynamicCount: 0,
nextDynamicCount: 0,
}
const info = child._ssrDynamicInfo
// Calculate the previous static and dynamic node counts
let foundStaticPrev = false
let dynamicCountPrev = 0
for (let j = i - 1; j >= 0; j--) {
const prevChild = filteredChildren[j]
if (isStaticChildNode(prevChild)) {
foundStaticPrev = true
break
}
// if the previous child has dynamic info, use it
else if (prevChild._ssrDynamicInfo) {
foundStaticPrev = prevChild._ssrDynamicInfo.hasStaticPrevious
dynamicCountPrev = prevChild._ssrDynamicInfo.prevDynamicCount + 1
break
}
dynamicCountPrev++
}
info.hasStaticPrevious = foundStaticPrev
info.prevDynamicCount = dynamicCountPrev
// Calculate the number of static and dynamic nodes afterwards
let foundStaticNext = false
let dynamicCountNext = 0
for (let j = i + 1; j < filteredChildren.length; j++) {
const nextChild = filteredChildren[j]
if (isStaticChildNode(nextChild)) {
foundStaticNext = true
break
}
// if the next child has dynamic info, use it
else if (nextChild._ssrDynamicInfo) {
foundStaticNext = nextChild._ssrDynamicInfo.hasStaticNext
dynamicCountNext = nextChild._ssrDynamicInfo.nextDynamicCount + 1
break
}
dynamicCountNext++
}
info.hasStaticNext = foundStaticNext
info.nextDynamicCount = dynamicCountNext
}
}
/**
* Check if a node should be processed as dynamic.
* This is primarily used in Vapor mode hydration to wrap dynamic parts
* with markers (`<!--[[-->` and `<!--]]-->`).
* The purpose is to distinguish the boundaries of nodes during hydration
*
* 1. two consecutive dynamic nodes should only wrap the second one
* <element>
* <element/> // Static previous sibling
* <Comp/> // Dynamic node (current)
* <Comp/> // Dynamic next sibling
* <element/> // Static next sibling
* <element/> // Static node
* <Comp/> // Dynamic node -> should NOT be wrapped
* <Comp/> // Dynamic node -> should be wrapped
* <element/> // Static node
* </element>
*
* 2. three or more consecutive dynamic nodes should only wrap the
* middle nodes, leaving the first and last static.
* <element>
* <element/> // Static node
* <Comp/> // Dynamic node -> should NOT be wrapped
* <Comp/> // Dynamic node -> should be wrapped
* <Comp/> // Dynamic node -> should be wrapped
* <Comp/> // Dynamic node -> should NOT be wrapped
* <element/> // Static node
*/
function shouldProcessAsDynamic(
function shouldProcessChildAsDynamic(
parent: { tag?: string; children: TemplateChildNode[] },
node: TemplateChildNode,
node: TemplateChildNode & { _ssrDynamicInfo?: DynamicInfo },
): boolean {
// 1. Must be a dynamic node type
if (isStaticChildNode(node)) return false
// 2. Must be inside a parent element
// must be inside a parent element
if (!parent.tag) return false
const children = parent.children.filter(
child => !(child.type === NodeTypes.TEXT && !child.content.trim()),
)
const len = children.length
const index = children.indexOf(node)
// must has dynamic info
const { _ssrDynamicInfo: info } = node
if (!info) return false
// 3. Check for a static previous sibling
let hasStaticPreviousSibling = false
if (index > 0) {
for (let i = index - 1; i >= 0; i--) {
if (isStaticChildNode(children[i])) {
hasStaticPreviousSibling = true
break
}
}
}
if (!hasStaticPreviousSibling) return false
const {
hasStaticPrevious,
hasStaticNext,
prevDynamicCount,
nextDynamicCount,
} = info
// 4. Check for a static next sibling
let hasStaticNextSibling = false
if (index > -1 && index < len - 1) {
for (let i = index + 1; i < len; i++) {
if (isStaticChildNode(children[i])) {
hasStaticNextSibling = true
break
}
}
}
if (!hasStaticNextSibling) return false
// must have static nodes on both sides
if (!hasStaticPrevious || !hasStaticNext) return false
// 5. Calculate the number and location of continuous dynamic nodes
let dynamicNodeCount = 1 // The current node is counted as one
let prevDynamicCount = 0
let nextDynamicCount = 0
const dynamicNodeCount = 1 + prevDynamicCount + nextDynamicCount
// Count consecutive dynamic nodes forward
for (let i = index - 1; i >= 0; i--) {
if (!isStaticChildNode(children[i])) {
prevDynamicCount++
} else {
break
}
}
// Count consecutive dynamic nodes backwards
for (let i = index + 1; i < len; i++) {
if (!isStaticChildNode(children[i])) {
nextDynamicCount++
} else {
break
}
}
dynamicNodeCount = 1 + prevDynamicCount + nextDynamicCount
// For two consecutive dynamic nodes, mark both as dynamic
// For two consecutive dynamic nodes, mark the second one as dynamic
if (dynamicNodeCount === 2) {
return prevDynamicCount > 0 || nextDynamicCount > 0
return prevDynamicCount > 0
}
// For three or more dynamic nodes, only mark the intermediate nodes as dynamic
// For three or more dynamic nodes, mark the intermediate node as dynamic
else if (dynamicNodeCount >= 3) {
return prevDynamicCount > 0 && nextDynamicCount > 0
}

View File

@ -1844,19 +1844,33 @@ describe('SSR hydration', () => {
})
describe('dynamic anchor', () => {
test('consecutive components', () => {
test('two consecutive components', () => {
const Comp = {
render() {
return createTextVNode('foo')
},
}
const { vnode, container } = mountWithHydration(
`<div><span></span><!--[[-->foo<!--]]--><!--[[-->foo<!--]]--><span></span></div>`,
`<div><span></span>foo<!--[[-->foo<!--]]--><span></span></div>`,
() => h('div', null, [h('span'), h(Comp), h(Comp), h('span')]),
)
expect(vnode.el).toBe(container.firstChild)
expect(`Hydration children mismatch`).not.toHaveBeenWarned()
})
test('multiple consecutive components', () => {
const Comp = {
render() {
return createTextVNode('foo')
},
}
const { vnode, container } = mountWithHydration(
`<div><span></span>foo<!--[[-->foo<!--]]-->foo<span></span></div>`,
() => h('div', null, [h('span'), h(Comp), h(Comp), h(Comp), h('span')]),
)
expect(vnode.el).toBe(container.firstChild)
expect(`Hydration children mismatch`).not.toHaveBeenWarned()
})
})
describe('mismatch handling', () => {

View File

@ -280,13 +280,13 @@ describe('Vapor Mode hydration', () => {
},
)
expect(container.innerHTML).toMatchInlineSnapshot(
`"<div><span></span><!--[[-->foo<!--]]--><!--[[-->foo<!--]]--><span></span></div>"`,
`"<div><span></span>foo<!--[[-->foo<!--]]--><span></span></div>"`,
)
data.value = 'bar'
await nextTick()
expect(container.innerHTML).toMatchInlineSnapshot(
`"<div><span></span><!--[[-->bar<!--]]--><!--[[-->bar<!--]]--><span></span></div>"`,
`"<div><span></span>bar<!--[[-->bar<!--]]--><span></span></div>"`,
)
})
@ -385,13 +385,13 @@ describe('Vapor Mode hydration', () => {
},
)
expect(container.innerHTML).toMatchInlineSnapshot(
`"<div><span></span><!--[[--><!--[--><div>foo</div>-foo<!--]--><!--]]--><!--[[--><!--[--><div>foo</div>-foo<!--]--><!--]]--><span></span></div>"`,
`"<div><span></span><!--[--><div>foo</div>-foo<!--]--><!--[[--><!--[--><div>foo</div>-foo<!--]--><!--]]--><span></span></div>"`,
)
data.value = 'bar'
await nextTick()
expect(container.innerHTML).toMatchInlineSnapshot(
`"<div><span></span><!--[[--><!--[--><div>bar</div>-bar<!--]--><!--]]--><!--[[--><!--[--><div>bar</div>-bar<!--]--><!--]]--><span></span></div>"`,
`"<div><span></span><!--[--><div>bar</div>-bar<!--]--><!--[[--><!--[--><div>bar</div>-bar<!--]--><!--]]--><span></span></div>"`,
)
})

View File

@ -42,8 +42,13 @@ function _next(node: Node): Node {
/*! #__NO_SIDE_EFFECTS__ */
function __next(node: Node): Node {
// process fragment as a single node
if (node && isComment(node, '[')) {
// treat dynamic node (<!--[[-->...<!--]]-->) as a single node
if (node && isComment(node, '[[')) {
node = locateEndAnchor(node, '[[', ']]')!
}
// treat dynamic node (<!--[-->...<!--]-->) as a single node
else if (node && isComment(node, '[')) {
node = locateEndAnchor(node)!
}