8000 feat(vapor/hydration): handle component with anchor insertion · vuejs/core@e704d07 · GitHub
[go: up one dir, main page]

Skip to content

Commit e704d07

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

File tree

6 files changed

+202
-48
lines changed

6 files changed

+202
-48
lines changed

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

Lines changed: 42 additions & 0 deletions
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 has static siblings', () => {
402+
expect(
403+
getCompiledString(`
404+
<div>
405+
<div/>
406+
<Comp1/>
407+
<div/>
408+
</div>
409+
`),
410+
).toMatchInlineSnapshot(`
4 8000 11+
"\`<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 component', () => {
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

Lines changed: 45 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -255,6 +255,12 @@ export function ssrProcessComponent(
255255
node.ssrCodegenNode.arguments.push(`_scopeId`)
256256
}
257257

258+
// `<!--[[-->` is used to mark the start of a dynamic child insertion point
259+
// for the client-side hydration.
260+
const hasStaticSibling = shouldAddDynamicAnchor(parent, node)
261+
if (hasStaticSibling) {
262+
context.pushStatement(createCallExpression(`_push`, [`"<!--[[-->"`]))
263+
}
258264
if (typeof component === 'string') {
259265
// static component
260266
context.pushStatement(
@@ -265,6 +271,9 @@ export function ssrProcessComponent(
265271
// the codegen node is a `renderVNode` call
266272
context.pushStatement(node.ssrCodegenNode)
267273
}
274+
if (hasStaticSibling) {
275+
context.pushStatement(createCallExpression(`_push`, [`"<!--]]-->"`]))
276+
}
268277
}
269278
}
270279

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

packages/runtime-core/src/hydration.ts

Lines changed: 17 additions & 3 deletions
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

Lines changed: 14 additions & 7 deletions
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

Lines changed: 67 additions & 36 deletions
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+
}

packages/runtime-vapor/src/insertionState.ts

Lines changed: 17 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,10 @@
1-
export let insertionParent: ParentNode | undefined
2-
export let insertionAnchor: Node | 0 | undefined
1+
export let insertionParent:
2+
| (ParentNode & {
3+
// cached the last dynamic start anchor
4+
lds?: Anchor
5+
})
6+
| undefined
7+
export let insertionAnchor: Node | 0 | undefined | null
38

49
/**
510
* This function is called before a block type that requires insertion
@@ -14,3 +19,13 @@ export function setInsertionState(parent: ParentNode, anchor?: Node | 0): void {
1419
export function resetInsertionState(): void {
1520
insertionParent = insertionAnchor = undefined
1621
}
22+
23+
export function setInsertionAnchor(anchor: Node | null): void {
24+
insertionAnchor = anchor
25+
}
26+
27+
export type Anchor = Comment & {
28+
// cached matching fragment start to avoid repeated traversal
29+
// on nested fragments
30+
$fs?: Anchor
31+
}

0 commit comments

Comments
 (0)
0