Skip to content

Commit e5dd701

Browse files
committed
feat(vapor/hydration): handle component with anchor insertion
1 parent 9ab8e4c commit e5dd701

File tree

7 files changed

+233
-48
lines changed

7 files changed

+233
-48
lines changed

packages/compiler-ssr/__tests__/ssrElement.spec.ts

+42
Original file line numberDiff line numberDiff line change
@@ -396,4 +396,46 @@ describe('ssr: element', () => {
396396
`)
397397
})
398398
})
399+
400+
describe('dynamic child anchor', () => {
401+
test('component with element siblings', () => {
402+
expect(
403+
getCompiledString(`
404+
<div>
405+
<div/>
406+
<Comp1/>
407+
<div/>
408+
</div>
409+
`),
410+
).toMatchInlineSnapshot(`
411+
"\`<div><div></div>\`)
412+
_push("<!--[[-->")
413+
_push(_ssrRenderComponent(_component_Comp1, null, null, _parent))
414+
_push("<!--]]-->")
415+
_push(\`<div></div></div>\`"
416+
`)
417+
})
418+
419+
test('with consecutive components', () => {
420+
expect(
421+
getCompiledString(`
422+
<div>
423+
<div/>
424+
<Comp1/>
425+
<Comp2/>
426+
<div/>
427+
</div>
428+
`),
429+
).toMatchInlineSnapshot(`
430+
"\`<div><div></div>\`)
431+
_push("<!--[[-->")
432+
_push(_ssrRenderComponent(_component_Comp1, null, null, _parent))
433+
_push("<!--]]-->")
434+
_push("<!--[[-->")
435+
_push(_ssrRenderComponent(_component_Comp2, null, null, _parent))
436+
_push("<!--]]-->")
437+
_push(\`<div></div></div>\`"
438+
`)
439+
})
440+
})
399441
})

packages/compiler-ssr/src/transforms/ssrTransformComponent.ts

+46
Original file line numberDiff line numberDiff line change
@@ -255,6 +255,13 @@ export function ssrProcessComponent(
255255
node.ssrCodegenNode.arguments.push(`_scopeId`)
256256
}
257257

258+
// `<!--[[-->` marks the start of the dynamic children
259+
// Only used in Vapor hydration, VDOM hydration
260+
// skips this marker.
261+
const needDynamicAnchor = shouldAddDynamicAnchor(parent, node)
262+
if (needDynamicAnchor) {
263+
context.pushStatement(createCallExpression(`_push`, [`"<!--[[-->"`]))
264+
}
258265
if (typeof component === 'string') {
259266
// static component
260267
context.pushStatement(
@@ -265,6 +272,9 @@ export function ssrProcessComponent(
265272
// the codegen node is a `renderVNode` call
266273
context.pushStatement(node.ssrCodegenNode)
267274
}
275+
if (needDynamicAnchor) {
276+
context.pushStatement(createCallExpression(`_push`, [`"<!--]]-->"`]))
277+
}
268278
}
269279
}
270280

@@ -384,3 +394,39 @@ function clone(v: any): any {
384394
return v
385395
}
386396
}
397+
398+
function shouldAddDynamicAnchor(
399+
parent: { tag?: string; children: TemplateChildNode[] },
400+
node: TemplateChildNode,
401+
): boolean {
402+
if (!parent.tag) return false
403+
404+
const children = parent.children
405+
const len = children.length
406+
const index = children.indexOf(node)
407+
408+
const isStaticElement = (c: TemplateChildNode): boolean =>
409+
c.type === NodeTypes.ELEMENT && c.tagType !== ElementTypes.COMPONENT
410+
411+
let hasStaticPreviousSibling = false
412+
if (index > 0) {
413+
for (let i = index - 1; i >= 0; i--) {
414+
if (isStaticElement(children[i])) {
415+
hasStaticPreviousSibling = true
416+
break
417+
}
418+
}
419+
}
420+
421+
let hasStaticNextSibling = false
422+
if (hasStaticPreviousSibling && index > -1 && index < len - 1) {
423+
for (let i = index + 1; i < len; i++) {
424+
if (isStaticElement(children[i])) {
425+
hasStaticNextSibling = true
426+
break
427+
}
428+
}
429+
}
430+
431+
return hasStaticPreviousSibling && hasStaticNextSibling
432+
}

packages/runtime-core/__tests__/hydration.spec.ts

+30
Original file line numberDiff line numberDiff line change
@@ -1843,6 +1843,36 @@ describe('SSR hydration', () => {
18431843
}
18441844
})
18451845

1846+
describe('dynamic child anchor', () => {
1847+
test('component with element siblings', () => {
1848+
const Comp = {
1849+
render() {
1850+
return createTextVNode('foo')
1851+
},
1852+
}
1853+
const { vnode, container } = mountWithHydration(
1854+
`<div><span></span><!--[[-->foo<!--]]--><span></span></div>`,
1855+
() => h('div', null, [h('span'), h(Comp), h('span')]),
1856+
)
1857+
expect(vnode.el).toBe(container.firstChild)
1858+
expect(`Hydration children mismatch`).not.toHaveBeenWarned()
1859+
})
1860+
1861+
test('with consecutive components', () => {
1862+
const Comp = {
1863+
render() {
1864+
return createTextVNode('foo')
1865+
},
1866+
}
1867+
const { vnode, container } = mountWithHydration(
1868+
`<div><span></span><!--[[-->foo<!--]]--><!--[[-->foo<!--]]--><span></span></div>`,
1869+
() => h('div', null, [h('span'), h(Comp), h(Comp), h('span')]),
1870+
)
1871+
expect(vnode.el).toBe(container.firstChild)
1872+
expect(`Hydration children mismatch`).not.toHaveBeenWarned()
1873+
})
1874+
})
1875+
18461876
describe('mismatch handling', () => {
18471877
test('text node', () => {
18481878
const { container } = mountWithHydration(`foo`, () => 'bar')

packages/runtime-core/src/hydration.ts

+17-3
Original file line numberDiff line numberDiff line change
@@ -111,14 +111,27 @@ export function createHydrationFunctions(
111111
o: {
112112
patchProp,
113113
createText,
114-
nextSibling,
114+
nextSibling: next,
115115
parentNode,
116116
remove,
117117
insert,
118118
createComment,
119119
},
120120
} = rendererInternals
121121

122+
function isDynamicAnchor(node: Node): boolean {
123+
return isComment(node) && (node.data === '[[' || node.data === ']]')
124+
}
125+
126+
function nextSibling(node: Node) {
127+
let n = next(node)
128+
// skip dynamic child anchor
129+
if (n && isDynamicAnchor(n)) {
130+
n = next(n)
131+
}
132+
return n
133+
}
134+
122135
const hydrate: RootHydrateFunction = (vnode, container) => {
123136
if (!container.hasChildNodes()) {
124137
;(__DEV__ || __FEATURE_PROD_HYDRATION_MISMATCH_DETAILS__) &&
@@ -145,6 +158,7 @@ export function createHydrationFunctions(
145158
slotScopeIds: string[] | null,
146159
optimized = false,
147160
): Node | null => {
161+
if (isDynamicAnchor(node)) node = nextSibling(node)!
148162
optimized = optimized || !!vnode.dynamicChildren
149163
const isFragmentStart = isComment(node) && node.data === '['
150164
const onMismatch = () =>
@@ -451,7 +465,7 @@ export function createHydrationFunctions(
451465

452466
// The SSRed DOM contains more nodes than it should. Remove them.
453467
const cur = next
454-
next = next.nextSibling
468+
next = nextSibling(next)
455469
remove(cur)
456470
}
457471
} else if (shapeFlag & ShapeFlags.TEXT_CHILDREN) {
@@ -553,7 +567,7 @@ export function createHydrationFunctions(
553567
}
554568
}
555569

556-
return el.nextSibling
570+
return nextSibling(el)
557571
}
558572

559573
const hydrateChildren = (

packages/runtime-vapor/__tests__/hydration.spec.ts

+14-7
Original file line numberDiff line numberDiff line change
@@ -239,8 +239,7 @@ describe('Vapor Mode hydration', () => {
239239
)
240240
})
241241

242-
// problem is the <!> placeholder does not exist in SSR output
243-
test.todo('component with anchor insertion', async () => {
242+
test('component with anchor insertion', async () => {
244243
const { container, data } = await testHydration(
245244
`
246245
<template>
@@ -255,14 +254,18 @@ describe('Vapor Mode hydration', () => {
255254
Child: `<template>{{ data }}</template>`,
256255
},
257256
)
258-
expect(container.innerHTML).toMatchInlineSnapshot()
257+
expect(container.innerHTML).toMatchInlineSnapshot(
258+
`"<div><span></span><!--[[-->foo<!--]]--><span></span></div>"`,
259+
)
259260

260261
data.value = 'bar'
261262
await nextTick()
262-
expect(container.innerHTML).toMatchInlineSnapshot()
263+
expect(container.innerHTML).toMatchInlineSnapshot(
264+
`"<div><span></span><!--[[-->bar<!--]]--><span></span></div>"`,
265+
)
263266
})
264267

265-
test.todo('consecutive component with anchor insertion', async () => {
268+
test('consecutive component with anchor insertion', async () => {
266269
const { container, data } = await testHydration(
267270
`<template>
268271
<div>
@@ -277,11 +280,15 @@ describe('Vapor Mode hydration', () => {
277280
Child: `<template>{{ data }}</template>`,
278281
},
279282
)
280-
expect(container.innerHTML).toMatchInlineSnapshot()
283+
expect(container.innerHTML).toMatchInlineSnapshot(
284+
`"<div><span></span><!--[[-->foo<!--]]--><!--[[-->foo<!--]]--><span></span></div>"`,
285+
)
281286

282287
data.value = 'bar'
283288
await nextTick()
284-
expect(container.innerHTML).toMatchInlineSnapshot()
289+
expect(container.innerHTML).toMatchInlineSnapshot(
290+
`"<div><span></span><!--[[-->bar<!--]]--><!--[[-->bar<!--]]--><span></span></div>"`,
291+
)
285292
})
286293

287294
test.todo('if')

packages/runtime-vapor/src/dom/hydration.ts

+67-36
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
import { warn } from '@vue/runtime-dom'
22
import {
3+
type Anchor,
34
insertionAnchor,
45
insertionParent,
56
resetInsertionState,
@@ -36,12 +37,6 @@ export function withHydration(container: ParentNode, fn: () => void): void {
3637
export let adoptTemplate: (node: Node, template: string) => Node | null
3738
export let locateHydrationNode: () => void
3839

39-
type Anchor = Comment & {
40-
// cached matching fragment start to avoid repeated traversal
41-
// on nested fragments
42-
$fs?: Anchor
43-
}
44-
4540
const isComment = (node: Node, data: string): node is Anchor =>
4641
node.nodeType === 8 && (node as Comment).data === data
4742

@@ -77,41 +72,48 @@ function adoptTemplateImpl(node: Node, template: string): Node | null {
7772

7873
function locateHydrationNodeImpl() {
7974
let node: Node | null
80-
8175
// prepend / firstChild
8276
if (insertionAnchor === 0) {
8377
node = child(insertionParent!)
8478
} else {
85-
node = insertionAnchor
86-
? insertionAnchor.previousSibling
87-
: insertionParent
88-
? insertionParent.lastChild
89-
: currentHydrationNode
90-
91-
if (node && isComment(node, ']')) {
92-
// fragment backward search
93-
if (node.$fs) {
94-
// already cached matching fragment start
95-
node = node.$fs
96-
} else {
97-
let cur: Node | null = node
98-
let curFragEnd = node
99-
let fragDepth = 0
100-
node = null
101-
while (cur) {
102-
cur = cur.previousSibling
103-
if (cur) {
104-
if (isComment(cur, '[')) {
105-
curFragEnd.$fs = cur
106-
if (!fragDepth) {
107-
node = cur
108-
break
109-
} else {
110-
fragDepth--
79+
// dynamic child anchor `<!--[[-->`
80+
if (insertionAnchor && isDynamicStart(insertionAnchor)) {
81+
const anchor = (insertionParent!.lds = insertionParent!.lds
82+
? // continuous dynamic children, the next dynamic start must exist
83+
locateNextDynamicStart(insertionParent!.lds)!
84+
: insertionAnchor)
85+
node = anchor.nextSibling
86+
} else {
87+
node = insertionAnchor
88+
? insertionAnchor.previousSibling
89+
: insertionParent
90+
? insertionParent.lastChild
91+
: currentHydrationNode
92+
if (node && isComment(node, ']')) {
93+
// fragment backward search
94+
if (node.$fs) {
95+
// already cached matching fragment start
96+
node = node.$fs
97+
} else {
98+
let cur: Node | null = node
99+
let curFragEnd = node
100+
let fragDepth = 0
101+
node = null
102+
while (cur) {
103+
cur = cur.previousSibling
104+
if (cur) {
105+
if (isComment(cur, '[')) {
106+
curFragEnd.$fs = cur
107+
if (!fragDepth) {
108+
node = cur
109+
break
110+
} else {
111+
fragDepth--
112+
}
113+
} else if (isComment(cur, ']')) {
114+
curFragEnd = cur
115+
fragDepth++
111116
}
112-
} else if (isComment(cur, ']')) {
113-
curFragEnd = cur
114-
fragDepth++
115117
}
116118
}
117119
}
@@ -127,3 +129,32 @@ function locateHydrationNodeImpl() {
127129
resetInsertionState()
128130
currentHydrationNode = node
129131
}
132+
133+
function isDynamicStart(node: Node): node is Anchor {
134+
return isComment(node, '[[')
135+
}
136+
137+
function locateNextDynamicStart(anchor: Anchor): Anchor | undefined {
138+
let cur: Node | null = anchor
139+
let end = null
140+
let depth = 0
141+
while (cur) {
142+
cur = cur.nextSibling
143+
if (cur) {
144+
if (isComment(cur, '[[')) {
145+
depth++
146+
} else if (isComment(cur, ']]')) {
147+
if (!depth) {
148+
end = cur
149+
break
150+
} else {
151+
depth--
152+
}
153+
}
154+
}
155+
}
156+
157+
if (end) {
158+
return end!.nextSibling as Anchor
159+
}
160+
}

0 commit comments

Comments
 (0)