Skip to content

Commit bfaa6dd

Browse files
committed
wip: create anchor for DynamicFragment when necessary
1 parent 8eedd1e commit bfaa6dd

File tree

6 files changed

+66
-52
lines changed

6 files changed

+66
-52
lines changed

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

Lines changed: 10 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -1195,11 +1195,15 @@ describe('Vapor Mode hydration', () => {
11951195

11961196
data.value = 'b'
11971197
await nextTick()
1198-
expect(container.innerHTML).toBe(`<div>bar</div><!--${anchorLabel}-->`)
1198+
expect(container.innerHTML).toBe(
1199+
`<div>bar</div><!--${anchorLabel}--><!--${anchorLabel}-->`,
1200+
)
11991201

12001202
data.value = 'c'
12011203
await nextTick()
1202-
expect(container.innerHTML).toBe(`<div>baz</div><!--${anchorLabel}-->`)
1204+
expect(container.innerHTML).toBe(
1205+
`<div>baz</div><!--${anchorLabel}--><!--${anchorLabel}-->`,
1206+
)
12031207

12041208
data.value = 'a'
12051209
await nextTick()
@@ -1390,13 +1394,13 @@ describe('Vapor Mode hydration', () => {
13901394
data.value = 'b'
13911395
await nextTick()
13921396
expect(container.innerHTML).toBe(
1393-
`<span>b child2</span><!--${anchorLabel}-->`,
1397+
`<span>b child2</span><!--${anchorLabel}--><!--${anchorLabel}-->`,
13941398
)
13951399

13961400
data.value = 'c'
13971401
await nextTick()
13981402
expect(container.innerHTML).toBe(
1399-
`<span>c child3</span><!--${anchorLabel}-->`,
1403+
`<span>c child3</span><!--${anchorLabel}--><!--${anchorLabel}-->`,
14001404
)
14011405

14021406
data.value = 'a'
@@ -2762,13 +2766,13 @@ describe('Vapor Mode hydration', () => {
27622766
)
27632767

27642768
expect(container.innerHTML).toBe(
2765-
`<div><!--[--><!--]--><!--slot--><div>foo</div></div>`,
2769+
`<div><!--[--><!--]--><!--slot--><!--slot--><!--slot--><!--slot--><div>foo</div></div>`,
27662770
)
27672771

27682772
data.foo = 'bar'
27692773
await nextTick()
27702774
expect(container.innerHTML).toBe(
2771-
`<div><!--[--><!--]--><!--slot--><div>bar</div></div>`,
2775+
`<div><!--[--><!--]--><!--slot--><!--slot--><!--slot--><!--slot--><div>bar</div></div>`,
27722776
)
27732777
})
27742778
})

packages/runtime-vapor/src/apiCreateIf.ts

Lines changed: 5 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
import { IF_ANCHOR_LABEL } from '@vue/shared'
1+
import { ELSE_IF_ANCHOR_LABEL, IF_ANCHOR_LABEL } from '@vue/shared'
22
import { type Block, type BlockFn, insert } from './block'
33
import { advanceHydrationNode, isHydrating } from './dom/hydration'
44
import {
@@ -56,8 +56,10 @@ export function createIf(
5656
frag = condition() ? b1() : b2 ? b2() : []
5757
} else {
5858
frag =
59-
(isHydrating || __DEV__) && !elseIf
60-
? new DynamicFragment(IF_ANCHOR_LABEL)
59+
isHydrating || __DEV__
60+
? new DynamicFragment(
61+
elseIf && isHydrating ? ELSE_IF_ANCHOR_LABEL : IF_ANCHOR_LABEL,
62+
)
6163
: new DynamicFragment()
6264
if (isHydrating) {
6365
;(frag as DynamicFragment).teardown = () => {

packages/runtime-vapor/src/block.ts

Lines changed: 14 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -14,11 +14,7 @@ import {
1414
} from '@vue/runtime-dom'
1515
import { isHydrating } from './dom/hydration'
1616
import { getInheritedScopeIds } from '@vue/runtime-dom'
17-
import {
18-
type DynamicFragment,
19-
type VaporFragment,
20-
isFragment,
21-
} from './fragment'
17+
import { DynamicFragment, type VaporFragment, isFragment } from './fragment'
2218

2319
export interface TransitionOptions {
2420
$key?: any
@@ -169,6 +165,19 @@ export function normalizeAnchor(node: Block): Node | undefined {
169165
}
170166
}
171167

168+
export function findLastChild(node: Block): Node | undefined | null {
169+
if (node && node instanceof Node) {
170+
return node
171+
} else if (isArray(node)) {
172+
return findLastChild(node[node.length - 1])
173+
} else if (isVaporComponent(node)) {
174+
return findLastChild(node.block!)
175+
} else {
176+
if (node instanceof DynamicFragment) return node.anchor
177+
return findLastChild(node.nodes!)
178+
}
179+
}
180+
172181
/**
173182
* dev / test only
174183
*/

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

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -203,7 +203,7 @@ export function locateVaporFragmentAnchor(
203203
node: Node,
204204
anchorLabel: string,
205205
): Comment | null {
206-
while (node) {
206+
while (node && node.nodeType === 8) {
207207
if (isComment(node, anchorLabel)) return node
208208
node = node.nextSibling!
209209
}

packages/runtime-vapor/src/fragment.ts

Lines changed: 35 additions & 37 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@ import {
55
type BlockFn,
66
type TransitionOptions,
77
type VaporTransitionHooks,
8+
findLastChild,
89
insert,
910
isValidBlock,
1011
remove,
@@ -13,6 +14,7 @@ import type { TransitionHooks } from '@vue/runtime-dom'
1314
import {
1415
advanceHydrationNode,
1516
currentHydrationNode,
17+
isComment,
1618
isHydrating,
1719
locateHydrationNode,
1820
locateVaporFragmentAnchor,
@@ -22,7 +24,7 @@ import {
2224
applyTransitionLeaveHooks,
2325
} from './components/Transition'
2426
import type { VaporComponentInstance } from './component'
25-
import { normalizeAnchor } from './block'
27+
import { ELSE_IF_ANCHOR_LABEL } from '@vue/shared'
2628

2729
export class VaporFragment<T extends Block = Block>
2830
implements TransitionOptions
@@ -76,7 +78,7 @@ export class DynamicFragment extends VaporFragment {
7678

7779
update(render?: BlockFn, key: any = render): void {
7880
if (key === this.current) {
79-
if (isHydrating && this.anchorLabel) this.hydrate(this.anchorLabel!, true)
81+
if (isHydrating) this.hydrate(this.anchorLabel!, true)
8082
return
8183
}
8284
this.current = key
@@ -142,49 +144,45 @@ export class DynamicFragment extends VaporFragment {
142144

143145
setActiveSub(prevSub)
144146

145-
if (isHydrating && this.anchorLabel) {
146-
// skip hydration for empty forwarded slots because
147-
// the server output does not include their anchors
148-
if (
149-
this.nodes instanceof DynamicFragment &&
150-
this.nodes.forwarded &&
151-
!isValidBlock(this.nodes)
152-
) {
153-
return
154-
}
155-
this.hydrate(this.anchorLabel)
147+
if (isHydrating) {
148+
this.hydrate(this.anchorLabel!, false)
156149
}
157150
}
158151

159152
hydrate = (label: string, isEmpty: boolean = false): void => {
160-
// for `v-if="false"`, the node will be an empty comment, use it as the anchor.
161-
// otherwise, find next sibling vapor fragment anchor
162-
if (label === 'if' && isEmpty) {
163-
this.anchor = locateVaporFragmentAnchor(currentHydrationNode!, '')!
153+
const createAnchor = () => {
154+
const { parentNode, nextSibling } = findLastChild(this.nodes)!
155+
parentNode!.insertBefore(
156+
(this.anchor = createComment(label)),
157+
nextSibling,
158+
)
159+
}
160+
161+
// manually create anchors for:
162+
// 1. else-if branch
163+
// 2. empty forwarded slot
164+
// (not present in SSR output)
165+
if (
166+
label === ELSE_IF_ANCHOR_LABEL ||
167+
(this.nodes instanceof DynamicFragment &&
168+
this.nodes.forwarded &&
169+
!isValidBlock(this.nodes))
170+
) {
171+
createAnchor()
164172
} else {
165-
this.anchor = locateVaporFragmentAnchor(currentHydrationNode!, label)!
166-
// comment anchors are not included in ssr slot vnode fallback
167-
if (!this.anchor) {
168-
if (label === 'slot') {
173+
// for `v-if="false"`, the node will be an empty comment, use it as the anchor.
174+
// otherwise, find next sibling vapor fragment anchor
175+
if (label === 'if' && isEmpty && isComment(currentHydrationNode!, '')) {
176+
this.anchor = currentHydrationNode!
177+
} else {
178+
this.anchor = locateVaporFragmentAnchor(currentHydrationNode!, label)!
179+
if (!this.anchor && label === 'slot') {
169180
// fallback to fragment end anchor for
170181
this.anchor = locateVaporFragmentAnchor(currentHydrationNode!, ']')!
171-
} else {
172-
// create anchor
173-
if (isFragment(this.nodes) && this.nodes.anchor) {
174-
// nested vapor fragment
175-
const { parentNode, nextSibling } = this.nodes.anchor
176-
parentNode!.insertBefore(
177-
(this.anchor = __DEV__ ? createComment(label) : createTextNode()),
178-
nextSibling,
179-
)
180-
} else {
181-
const { parentNode, nextSibling } = normalizeAnchor(this.nodes)!
182-
parentNode!.insertBefore(
183-
(this.anchor = __DEV__ ? createComment(label) : createTextNode()),
184-
nextSibling,
185-
)
186-
}
187182
}
183+
184+
// anchors are not present in ssr slot vnode fallback
185+
if (!this.anchor) createAnchor()
188186
}
189187
}
190188

packages/shared/src/domAnchors.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@ export const DYNAMIC_START_ANCHOR_LABEL = '[['
22
export const DYNAMIC_END_ANCHOR_LABEL = ']]'
33

44
export const IF_ANCHOR_LABEL: string = 'if'
5+
export const ELSE_IF_ANCHOR_LABEL: string = 'else-if'
56
export const DYNAMIC_COMPONENT_ANCHOR_LABEL: string = 'dynamic-component'
67
export const FOR_ANCHOR_LABEL: string = 'for'
78
export const SLOT_ANCHOR_LABEL: string = 'slot'

0 commit comments

Comments
 (0)