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', () => { describe('dynamic anchor', () => {
test('consecutive components', () => { test('two consecutive components', () => {
expect( expect(
getCompiledString(` getCompiledString(`
<div> <div>
@ -409,12 +409,37 @@ describe('ssr: element', () => {
</div> </div>
`), `),
).toMatchInlineSnapshot(` ).toMatchInlineSnapshot(`
"\`<div><div></div><!--[[-->\`) "\`<div><div></div>\`)
_push(_ssrRenderComponent(_component_Comp1, null, null, _parent)) _push(_ssrRenderComponent(_component_Comp1, null, null, _parent))
_push(\`<!--]]--><!--[[-->\`) _push(\`<!--[[-->\`)
_push(_ssrRenderComponent(_component_Comp2, null, null, _parent)) _push(_ssrRenderComponent(_component_Comp2, null, null, _parent))
_push(\`<!--]]--><div></div></div>\`" _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 IfStatement,
type JSChildNode, type JSChildNode,
NodeTypes, NodeTypes,
type PlainElementNode,
type RootNode, type RootNode,
type TemplateChildNode, type TemplateChildNode,
type TemplateLiteral, type TemplateLiteral,
@ -166,10 +167,14 @@ export function processChildren(
context.pushStringPart(`<!--[-->`) 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++) { for (let i = 0; i < children.length; i++) {
const child = children[i] const child = children[i]
if (shouldProcessAsDynamic(parent, child)) { if (inElement && shouldProcessChildAsDynamic(parent, child)) {
processChildren( processChildren(
{ children: [child] }, { children: [child] },
context, context,
@ -274,87 +279,127 @@ const isStaticChildNode = (c: TemplateChildNode): boolean =>
c.type === NodeTypes.TEXT || c.type === NodeTypes.TEXT ||
c.type === NodeTypes.COMMENT 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. * Check if a node should be processed as dynamic.
* This is primarily used in Vapor mode hydration to wrap dynamic parts * This is primarily used in Vapor mode hydration to wrap dynamic parts
* with markers (`<!--[[-->` and `<!--]]-->`). * 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>
* <element/> // Static previous sibling * <element/> // Static node
* <Comp/> // Dynamic node (current) * <Comp/> // Dynamic node -> should NOT be wrapped
* <Comp/> // Dynamic next sibling * <Comp/> // Dynamic node -> should be wrapped
* <element/> // Static next sibling * <element/> // Static node
* </element> * </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[] }, parent: { tag?: string; children: TemplateChildNode[] },
node: TemplateChildNode, node: TemplateChildNode & { _ssrDynamicInfo?: DynamicInfo },
): boolean { ): boolean {
// 1. Must be a dynamic node type // must be inside a parent element
if (isStaticChildNode(node)) return false
// 2. Must be inside a parent element
if (!parent.tag) return false if (!parent.tag) return false
const children = parent.children.filter( // must has dynamic info
child => !(child.type === NodeTypes.TEXT && !child.content.trim()), const { _ssrDynamicInfo: info } = node
) if (!info) return false
const len = children.length
const index = children.indexOf(node)
// 3. Check for a static previous sibling const {
let hasStaticPreviousSibling = false hasStaticPrevious,
if (index > 0) { hasStaticNext,
for (let i = index - 1; i >= 0; i--) { prevDynamicCount,
if (isStaticChildNode(children[i])) { nextDynamicCount,
hasStaticPreviousSibling = true } = info
break
}
}
}
if (!hasStaticPreviousSibling) return false
// 4. Check for a static next sibling // must have static nodes on both sides
let hasStaticNextSibling = false if (!hasStaticPrevious || !hasStaticNext) return 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
// 5. Calculate the number and location of continuous dynamic nodes const dynamicNodeCount = 1 + prevDynamicCount + nextDynamicCount
let dynamicNodeCount = 1 // The current node is counted as one
let prevDynamicCount = 0
let nextDynamicCount = 0
// Count consecutive dynamic nodes forward // For two consecutive dynamic nodes, mark the second one as dynamic
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
if (dynamicNodeCount === 2) { 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) { else if (dynamicNodeCount >= 3) {
return prevDynamicCount > 0 && nextDynamicCount > 0 return prevDynamicCount > 0 && nextDynamicCount > 0
} }

View File

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

View File

@ -280,13 +280,13 @@ describe('Vapor Mode hydration', () => {
}, },
) )
expect(container.innerHTML).toMatchInlineSnapshot( expect(container.innerHTML).toMatchInlineSnapshot(
`"<div><span></span><!--[[-->foo<!--]]--><!--[[-->foo<!--]]--><span></span></div>"`, `"<div><span></span>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<!--]]--><span></span></div>"`, `"<div><span></span>bar<!--[[-->bar<!--]]--><span></span></div>"`,
) )
}) })
@ -385,13 +385,13 @@ describe('Vapor Mode hydration', () => {
}, },
) )
expect(container.innerHTML).toMatchInlineSnapshot( 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' data.value = 'bar'
await nextTick() await nextTick()
expect(container.innerHTML).toMatchInlineSnapshot( 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__ */ /*! #__NO_SIDE_EFFECTS__ */
function __next(node: Node): Node { function __next(node: Node): Node {
// process fragment as a single node // treat dynamic node (<!--[[-->...<!--]]-->) as a single node
if (node && isComment(node, '[')) { if (node && isComment(node, '[[')) {
node = locateEndAnchor(node, '[[', ']]')!
}
// treat dynamic node (<!--[-->...<!--]-->) as a single node
else if (node && isComment(node, '[')) {
node = locateEndAnchor(node)! node = locateEndAnchor(node)!
} }