wip: refactor hydration for v-if

This commit is contained in:
daiwei 2025-04-24 16:58:18 +08:00
parent 38d4889de7
commit e9c9e4903d
17 changed files with 274 additions and 67 deletions

View File

@ -245,6 +245,7 @@ describe('ssr: components', () => {
_push(\`<span\${_scopeId}></span>\`) _push(\`<span\${_scopeId}></span>\`)
}) })
_push(\`<!--]--></div>\`) _push(\`<!--]--></div>\`)
_push(\`<!--$-->\`)
} else { } else {
_push(\`<!---->\`) _push(\`<!---->\`)
} }
@ -268,6 +269,7 @@ describe('ssr: components', () => {
_push(\`<span\${_scopeId}></span>\`) _push(\`<span\${_scopeId}></span>\`)
}) })
_push(\`<!--]--></div>\`) _push(\`<!--]--></div>\`)
_push(\`<!--$-->\`)
} else { } else {
_push(\`<!---->\`) _push(\`<!---->\`)
} }
@ -361,6 +363,7 @@ describe('ssr: components', () => {
_push(\`\`) _push(\`\`)
if (false) { if (false) {
_push(\`<div\${_scopeId}></div>\`) _push(\`<div\${_scopeId}></div>\`)
_push(\`<!--$-->\`)
} else { } else {
_push(\`<!---->\`) _push(\`<!---->\`)
} }

View File

@ -29,6 +29,7 @@ describe('ssr: attrs fallthrough', () => {
_push(\`<!--[-->\`) _push(\`<!--[-->\`)
if (true) { if (true) {
_push(\`<div></div>\`) _push(\`<div></div>\`)
_push(\`<!--$-->\`)
} else { } else {
_push(\`<!---->\`) _push(\`<!---->\`)
} }

View File

@ -70,6 +70,7 @@ describe('ssr: inject <style vars>', () => {
const _cssVars = { style: { color: _ctx.color }} const _cssVars = { style: { color: _ctx.color }}
if (_ctx.ok) { if (_ctx.ok) {
_push(\`<div\${_ssrRenderAttrs(_mergeProps(_attrs, _cssVars))}></div>\`) _push(\`<div\${_ssrRenderAttrs(_mergeProps(_attrs, _cssVars))}></div>\`)
_push(\`<!--$-->\`)
} else { } else {
_push(\`<!--[--><div\${ _push(\`<!--[--><div\${
_ssrRenderAttrs(_cssVars) _ssrRenderAttrs(_cssVars)

View File

@ -153,6 +153,7 @@ describe('ssr: <slot>', () => {
return function ssrRender(_ctx, _push, _parent, _attrs) { return function ssrRender(_ctx, _push, _parent, _attrs) {
if (true) { if (true) {
_ssrRenderSlotInner(_ctx.$slots, "default", {}, null, _push, _parent, null, true) _ssrRenderSlotInner(_ctx.$slots, "default", {}, null, _push, _parent, null, true)
_push(\`<!--$-->\`)
} else { } else {
_push(\`<!---->\`) _push(\`<!---->\`)
} }

View File

@ -54,6 +54,7 @@ describe('transition-group', () => {
}) })
if (false) { if (false) {
_push(\`<div></div>\`) _push(\`<div></div>\`)
_push(\`<!--$-->\`)
} }
_push(\`</ul>\`) _push(\`</ul>\`)
}" }"
@ -123,6 +124,7 @@ describe('transition-group', () => {
}) })
if (_ctx.ok) { if (_ctx.ok) {
_push(\`<div>ok</div>\`) _push(\`<div>ok</div>\`)
_push(\`<!--$-->\`)
} }
_push(\`<!--]-->\`) _push(\`<!--]-->\`)
}" }"

View File

@ -8,6 +8,7 @@ describe('ssr: v-if', () => {
return function ssrRender(_ctx, _push, _parent, _attrs) { return function ssrRender(_ctx, _push, _parent, _attrs) {
if (_ctx.foo) { if (_ctx.foo) {
_push(\`<div\${_ssrRenderAttrs(_attrs)}></div>\`) _push(\`<div\${_ssrRenderAttrs(_attrs)}></div>\`)
_push(\`<!--$-->\`)
} else { } else {
_push(\`<!---->\`) _push(\`<!---->\`)
} }
@ -23,6 +24,7 @@ describe('ssr: v-if', () => {
return function ssrRender(_ctx, _push, _parent, _attrs) { return function ssrRender(_ctx, _push, _parent, _attrs) {
if (_ctx.foo) { if (_ctx.foo) {
_push(\`<div\${_ssrRenderAttrs(_attrs)}>hello<span>ok</span></div>\`) _push(\`<div\${_ssrRenderAttrs(_attrs)}>hello<span>ok</span></div>\`)
_push(\`<!--$-->\`)
} else { } else {
_push(\`<!---->\`) _push(\`<!---->\`)
} }
@ -38,6 +40,7 @@ describe('ssr: v-if', () => {
return function ssrRender(_ctx, _push, _parent, _attrs) { return function ssrRender(_ctx, _push, _parent, _attrs) {
if (_ctx.foo) { if (_ctx.foo) {
_push(\`<div\${_ssrRenderAttrs(_attrs)}></div>\`) _push(\`<div\${_ssrRenderAttrs(_attrs)}></div>\`)
_push(\`<!--$-->\`)
} else { } else {
_push(\`<span\${_ssrRenderAttrs(_attrs)}></span>\`) _push(\`<span\${_ssrRenderAttrs(_attrs)}></span>\`)
} }
@ -53,8 +56,10 @@ describe('ssr: v-if', () => {
return function ssrRender(_ctx, _push, _parent, _attrs) { return function ssrRender(_ctx, _push, _parent, _attrs) {
if (_ctx.foo) { if (_ctx.foo) {
_push(\`<div\${_ssrRenderAttrs(_attrs)}></div>\`) _push(\`<div\${_ssrRenderAttrs(_attrs)}></div>\`)
_push(\`<!--$-->\`)
} else if (_ctx.bar) { } else if (_ctx.bar) {
_push(\`<span\${_ssrRenderAttrs(_attrs)}></span>\`) _push(\`<span\${_ssrRenderAttrs(_attrs)}></span>\`)
_push(\`<!--$-->\`)
} else { } else {
_push(\`<!---->\`) _push(\`<!---->\`)
} }
@ -70,8 +75,10 @@ describe('ssr: v-if', () => {
return function ssrRender(_ctx, _push, _parent, _attrs) { return function ssrRender(_ctx, _push, _parent, _attrs) {
if (_ctx.foo) { if (_ctx.foo) {
_push(\`<div\${_ssrRenderAttrs(_attrs)}></div>\`) _push(\`<div\${_ssrRenderAttrs(_attrs)}></div>\`)
_push(\`<!--$-->\`)
} else if (_ctx.bar) { } else if (_ctx.bar) {
_push(\`<span\${_ssrRenderAttrs(_attrs)}></span>\`) _push(\`<span\${_ssrRenderAttrs(_attrs)}></span>\`)
_push(\`<!--$-->\`)
} else { } else {
_push(\`<p\${_ssrRenderAttrs(_attrs)}></p>\`) _push(\`<p\${_ssrRenderAttrs(_attrs)}></p>\`)
} }
@ -82,15 +89,16 @@ describe('ssr: v-if', () => {
test('<template v-if> (text)', () => { test('<template v-if> (text)', () => {
expect(compile(`<template v-if="foo">hello</template>`).code) expect(compile(`<template v-if="foo">hello</template>`).code)
.toMatchInlineSnapshot(` .toMatchInlineSnapshot(`
" "
return function ssrRender(_ctx, _push, _parent, _attrs) { return function ssrRender(_ctx, _push, _parent, _attrs) {
if (_ctx.foo) { if (_ctx.foo) {
_push(\`<!--[-->hello<!--]-->\`) _push(\`<!--[-->hello<!--]-->\`)
} else { _push(\`<!--$-->\`)
_push(\`<!---->\`) } else {
} _push(\`<!---->\`)
}" }
`) }"
`)
}) })
test('<template v-if> (single element)', () => { test('<template v-if> (single element)', () => {
@ -102,6 +110,7 @@ describe('ssr: v-if', () => {
return function ssrRender(_ctx, _push, _parent, _attrs) { return function ssrRender(_ctx, _push, _parent, _attrs) {
if (_ctx.foo) { if (_ctx.foo) {
_push(\`<div\${_ssrRenderAttrs(_attrs)}>hi</div>\`) _push(\`<div\${_ssrRenderAttrs(_attrs)}>hi</div>\`)
_push(\`<!--$-->\`)
} else { } else {
_push(\`<!---->\`) _push(\`<!---->\`)
} }
@ -118,6 +127,7 @@ describe('ssr: v-if', () => {
return function ssrRender(_ctx, _push, _parent, _attrs) { return function ssrRender(_ctx, _push, _parent, _attrs) {
if (_ctx.foo) { if (_ctx.foo) {
_push(\`<!--[--><div>hi</div><div>ho</div><!--]-->\`) _push(\`<!--[--><div>hi</div><div>ho</div><!--]-->\`)
_push(\`<!--$-->\`)
} else { } else {
_push(\`<!---->\`) _push(\`<!---->\`)
} }
@ -138,6 +148,7 @@ describe('ssr: v-if', () => {
_push(\`<div></div>\`) _push(\`<div></div>\`)
}) })
_push(\`<!--]-->\`) _push(\`<!--]-->\`)
_push(\`<!--$-->\`)
} else { } else {
_push(\`<!---->\`) _push(\`<!---->\`)
} }
@ -156,6 +167,7 @@ describe('ssr: v-if', () => {
return function ssrRender(_ctx, _push, _parent, _attrs) { return function ssrRender(_ctx, _push, _parent, _attrs) {
if (_ctx.foo) { if (_ctx.foo) {
_push(\`<!--[--><div>hi</div><div>ho</div><!--]-->\`) _push(\`<!--[--><div>hi</div><div>ho</div><!--]-->\`)
_push(\`<!--$-->\`)
} else { } else {
_push(\`<div\${_ssrRenderAttrs(_attrs)}></div>\`) _push(\`<div\${_ssrRenderAttrs(_attrs)}></div>\`)
} }

View File

@ -91,6 +91,7 @@ describe('ssr: v-model', () => {
? _ssrLooseContain(_ctx.model, _ctx.i) ? _ssrLooseContain(_ctx.model, _ctx.i)
: _ssrLooseEqual(_ctx.model, _ctx.i))) ? " selected" : "" : _ssrLooseEqual(_ctx.model, _ctx.i))) ? " selected" : ""
}></option>\`) }></option>\`)
_push(\`<!--$-->\`)
} else { } else {
_push(\`<!---->\`) _push(\`<!---->\`)
} }

View File

@ -295,8 +295,13 @@ function processChildrenDynamicInfo(
for (let i = 0; i < filteredChildren.length; i++) { for (let i = 0; i < filteredChildren.length; i++) {
const child = filteredChildren[i] const child = filteredChildren[i]
if (isStaticChildNode(child)) continue if (
isStaticChildNode(child) ||
// v-if has an anchor, which can be used to distinguish the boundary
child.type === NodeTypes.IF
) {
continue
}
child._ssrDynamicInfo = { child._ssrDynamicInfo = {
hasStaticPrevious: false, hasStaticPrevious: false,
hasStaticNext: false, hasStaticNext: false,

View File

@ -74,5 +74,13 @@ function processIfBranch(
(children.length !== 1 || children[0].type !== NodeTypes.ELEMENT) && (children.length !== 1 || children[0].type !== NodeTypes.ELEMENT) &&
// optimize away nested fragments when the only child is a ForNode // optimize away nested fragments when the only child is a ForNode
!(children.length === 1 && children[0].type === NodeTypes.FOR) !(children.length === 1 && children[0].type === NodeTypes.FOR)
return processChildrenAsStatement(branch, context, needFragmentWrapper) const statement = processChildrenAsStatement(
branch,
context,
needFragmentWrapper,
)
if (branch.condition) {
statement.body.push(createCallExpression(`_push`, ['`<!--$-->`']))
}
return statement
} }

View File

@ -598,14 +598,14 @@ describe('SSR hydration', () => {
const ctx: SSRContext = {} const ctx: SSRContext = {}
container.innerHTML = await renderToString(h(App), ctx) container.innerHTML = await renderToString(h(App), ctx)
expect(container.innerHTML).toBe( expect(container.innerHTML).toBe(
'<div><!--teleport start--><!--teleport end--></div>', '<div><!--teleport start--><!--teleport end--><!--$--></div>',
) )
teleportContainer.innerHTML = ctx.teleports!['#target'] teleportContainer.innerHTML = ctx.teleports!['#target']
// hydrate // hydrate
createSSRApp(App).mount(container) createSSRApp(App).mount(container)
expect(container.innerHTML).toBe( expect(container.innerHTML).toBe(
'<div><!--teleport start--><!--teleport end--></div>', '<div><!--teleport start--><!--teleport end--><!--$--></div>',
) )
expect(teleportContainer.innerHTML).toBe( expect(teleportContainer.innerHTML).toBe(
'<!--teleport start anchor--><span>Teleported Comp1</span><!--teleport anchor-->', '<!--teleport start anchor--><span>Teleported Comp1</span><!--teleport anchor-->',
@ -614,7 +614,7 @@ describe('SSR hydration', () => {
toggle.value = false toggle.value = false
await nextTick() await nextTick()
expect(container.innerHTML).toBe('<div><div>Comp2</div></div>') expect(container.innerHTML).toBe('<div><div>Comp2</div><!--$--></div>')
expect(teleportContainer.innerHTML).toBe('') expect(teleportContainer.innerHTML).toBe('')
}) })
@ -657,21 +657,21 @@ describe('SSR hydration', () => {
// server render // server render
container.innerHTML = await renderToString(h(App)) container.innerHTML = await renderToString(h(App))
expect(container.innerHTML).toBe( expect(container.innerHTML).toBe(
'<div><!--teleport start--><!--teleport end--></div>', '<div><!--teleport start--><!--teleport end--><!--$--></div>',
) )
expect(teleportContainer.innerHTML).toBe('') expect(teleportContainer.innerHTML).toBe('')
// hydrate // hydrate
createSSRApp(App).mount(container) createSSRApp(App).mount(container)
expect(container.innerHTML).toBe( expect(container.innerHTML).toBe(
'<div><!--teleport start--><!--teleport end--></div>', '<div><!--teleport start--><!--teleport end--><!--$--></div>',
) )
expect(teleportContainer.innerHTML).toBe('<span>Teleported Comp1</span>') expect(teleportContainer.innerHTML).toBe('<span>Teleported Comp1</span>')
expect(`Hydration children mismatch`).toHaveBeenWarned() expect(`Hydration children mismatch`).toHaveBeenWarned()
toggle.value = false toggle.value = false
await nextTick() await nextTick()
expect(container.innerHTML).toBe('<div><div>Comp2</div></div>') expect(container.innerHTML).toBe('<div><div>Comp2</div><!--$--></div>')
expect(teleportContainer.innerHTML).toBe('') expect(teleportContainer.innerHTML).toBe('')
}) })

View File

@ -84,10 +84,14 @@ const getContainerType = (
return undefined return undefined
} }
export function isDynamicAnchor(node: Node): boolean { export function isDynamicAnchor(node: Node): node is Comment {
return isComment(node) && (node.data === '[[' || node.data === ']]') return isComment(node) && (node.data === '[[' || node.data === ']]')
} }
export function isDynamicFragmentEndAnchor(node: Node): node is Comment {
return isComment(node) && node.data === '$'
}
export const isComment = (node: Node): node is Comment => export const isComment = (node: Node): node is Comment =>
node.nodeType === DOMNodeTypes.COMMENT node.nodeType === DOMNodeTypes.COMMENT
@ -125,8 +129,10 @@ export function createHydrationFunctions(
function nextSibling(node: Node) { function nextSibling(node: Node) {
let n = next(node) let n = next(node)
// skip dynamic anchors // skip if:
if (n && isDynamicAnchor(n)) { // - dynamic anchors (`<!--[-->`, `<!--]-->`)
// - dynamic fragment end anchors (`<!--$-->`)
if (n && (isDynamicAnchor(n) || isDynamicFragmentEndAnchor(n))) {
n = next(n) n = next(n)
} }
return n return n
@ -158,7 +164,9 @@ export function createHydrationFunctions(
slotScopeIds: string[] | null, slotScopeIds: string[] | null,
optimized = false, optimized = false,
): Node | null => { ): Node | null => {
if (isDynamicAnchor(node)) node = nextSibling(node)! if (isDynamicAnchor(node) || isDynamicFragmentEndAnchor(node)) {
node = nextSibling(node)!
}
optimized = optimized || !!vnode.dynamicChildren optimized = optimized || !!vnode.dynamicChildren
const isFragmentStart = isComment(node) && node.data === '[' const isFragmentStart = isComment(node) && node.data === '['
const onMismatch = () => const onMismatch = () =>

View File

@ -560,4 +560,4 @@ export { initFeatureFlags } from './featureFlags'
/** /**
* @internal * @internal
*/ */
export { isDynamicAnchor } from './hydration' export { isDynamicAnchor, isDynamicFragmentEndAnchor } from './hydration'

View File

@ -730,7 +730,97 @@ describe('Vapor Mode hydration', () => {
expect(container.innerHTML).toMatchInlineSnapshot(`"<!--if-->"`) expect(container.innerHTML).toMatchInlineSnapshot(`"<!--if-->"`)
}) })
test.todo('on component', async () => {}) test('on component', async () => {
const data = ref(true)
const { container } = await testHydration(
`<template>
<components.Child v-if="data"/>
</template>`,
{ Child: `<template>foo</template>` },
data,
)
expect(container.innerHTML).toMatchInlineSnapshot(`"foo<!--if-->"`)
data.value = false
await nextTick()
expect(container.innerHTML).toMatchInlineSnapshot(`"<!--if-->"`)
})
test('on component with anchor insertion', async () => {
const data = ref(true)
const { container } = await testHydration(
`<template>
<div>
<span/>
<components.Child v-if="data"/>
<span/>
</div>
</template>`,
{ Child: `<template>foo</template>` },
data,
)
expect(container.innerHTML).toMatchInlineSnapshot(
`"<div><span></span>foo<!--if--><span></span></div>"`,
)
data.value = false
await nextTick()
expect(container.innerHTML).toMatchInlineSnapshot(
`"<div><span></span><!--if--><span></span></div>"`,
)
})
test('consecutive v-if on component with anchor insertion', async () => {
const data = ref(true)
const { container } = await testHydration(
`<template>
<div>
<span/>
<components.Child v-if="data"/>
<components.Child v-if="data"/>
<span/>
</div>
</template>`,
{ Child: `<template>foo</template>` },
data,
)
expect(container.innerHTML).toMatchInlineSnapshot(
`"<div><span></span>foo<!--if-->foo<!--if--><span></span></div>"`,
)
data.value = false
await nextTick()
expect(container.innerHTML).toMatchInlineSnapshot(
`"<div><span></span><!--if--><!--if--><span></span></div>"`,
)
})
test('consecutive v-if on fragment component with anchor insertion', async () => {
const data = ref(true)
const { container } = await testHydration(
`<template>
<div>
<span/>
<components.Child v-if="data"/>
<components.Child v-if="data"/>
<span/>
</div>
</template>`,
{
Child: `<template><div>{{ data }}</div>-{{ data }}-</template>`,
},
data,
)
expect(container.innerHTML).toMatchInlineSnapshot(
`"<div><span></span><!--[--><div>true</div>-true-<!--]--><!--if--><!--[--><div>true</div>-true-<!--]--><!--if--><span></span></div>"`,
)
data.value = false
await nextTick()
expect(container.innerHTML).toMatchInlineSnapshot(
`"<div><span></span><!--[--><!--]--><!--if--><!--[--><!--]--><!--if--><span></span></div>"`,
)
})
}) })
test.todo('for') test.todo('for')

View File

@ -1,10 +1,5 @@
import { type Block, type BlockFn, DynamicFragment, insert } from './block' import { type Block, type BlockFn, DynamicFragment, insert } from './block'
import { import { isHydrating, locateHydrationNode } from './dom/hydration'
currentHydrationNode,
isComment,
isHydrating,
locateHydrationNode,
} from './dom/hydration'
import { insertionAnchor, insertionParent } from './insertionState' import { insertionAnchor, insertionParent } from './insertionState'
import { renderEffect } from './renderEffect' import { renderEffect } from './renderEffect'
@ -16,10 +11,8 @@ export function createIf(
): Block { ): Block {
const _insertionParent = insertionParent const _insertionParent = insertionParent
const _insertionAnchor = insertionAnchor const _insertionAnchor = insertionAnchor
let _currentHydrationNode
if (isHydrating) { if (isHydrating) {
locateHydrationNode() locateHydrationNode(true)
_currentHydrationNode = currentHydrationNode
} }
let frag: Block let frag: Block
@ -34,23 +27,5 @@ export function createIf(
insert(frag, _insertionParent, _insertionAnchor) insert(frag, _insertionParent, _insertionAnchor)
} }
// if the current hydration node is a comment, use it as an anchor
// otherwise need to insert the anchor node
// OR adjust ssr output to add anchor for v-if
else if (isHydrating && _currentHydrationNode) {
const parentNode = _currentHydrationNode.parentNode
if (parentNode) {
if (isComment(_currentHydrationNode, '')) {
if (__DEV__) _currentHydrationNode.data = 'if'
;(frag as DynamicFragment).anchor = _currentHydrationNode
} else {
parentNode.insertBefore(
(frag as DynamicFragment).anchor,
_currentHydrationNode.nextSibling,
)
}
}
}
return frag return frag
} }

View File

@ -5,9 +5,10 @@ import {
mountComponent, mountComponent,
unmountComponent, unmountComponent,
} from './component' } from './component'
import { createComment, createTextNode } from './dom/node' import { createComment, createTextNode, next } from './dom/node'
import { EffectScope, pauseTracking, resetTracking } from '@vue/reactivity' import { EffectScope, pauseTracking, resetTracking } from '@vue/reactivity'
import { isHydrating } from './dom/hydration' import { currentHydrationNode, isComment, isHydrating } from './dom/hydration'
import { isDynamicFragmentEndAnchor, warn } from '@vue/runtime-dom'
export type Block = export type Block =
| Node | Node
@ -30,15 +31,19 @@ export class VaporFragment {
} }
export class DynamicFragment extends VaporFragment { export class DynamicFragment extends VaporFragment {
anchor: Node anchor!: Node
scope: EffectScope | undefined scope: EffectScope | undefined
current?: BlockFn current?: BlockFn
fallback?: BlockFn fallback?: BlockFn
constructor(anchorLabel?: string) { constructor(anchorLabel?: string) {
super([]) super([])
this.anchor = if (isHydrating) {
__DEV__ && anchorLabel ? createComment(anchorLabel) : createTextNode() this.hydrate(anchorLabel)
} else {
this.anchor =
__DEV__ && anchorLabel ? createComment(anchorLabel) : createTextNode()
}
} }
update(render?: BlockFn, key: any = render): void { update(render?: BlockFn, key: any = render): void {
@ -75,6 +80,24 @@ export class DynamicFragment extends VaporFragment {
resetTracking() resetTracking()
} }
hydrate(label?: string): void {
// for v-if="false" the hydrationNode will be a empty comment node
// use it as anchor.
// otherwise, use the next sibling comment node as anchor
if (isComment(currentHydrationNode!, '')) {
this.anchor = currentHydrationNode
} else {
const anchor = next(currentHydrationNode!)
if (isDynamicFragmentEndAnchor(anchor)) {
this.anchor = anchor
} else if (__DEV__) {
// TODO warning
warn(`DynamicFragment anchor not found...`)
}
}
if (__DEV__ && label) (this.anchor as Comment).data = label
}
} }
export function isFragment(val: NonNullable<unknown>): val is VaporFragment { export function isFragment(val: NonNullable<unknown>): val is VaporFragment {

View File

@ -1,4 +1,4 @@
import { warn } from '@vue/runtime-dom' import { isDynamicFragmentEndAnchor, warn } from '@vue/runtime-dom'
import { import {
insertionAnchor, insertionAnchor,
insertionParent, insertionParent,
@ -10,6 +10,7 @@ import {
disableHydrationNodeLookup, disableHydrationNodeLookup,
enableHydrationNodeLookup, enableHydrationNodeLookup,
next, next,
prev,
} from './node' } from './node'
export let isHydrating = false export let isHydrating = false
@ -41,7 +42,7 @@ export function withHydration(container: ParentNode, fn: () => void): void {
} }
export let adoptTemplate: (node: Node, template: string) => Node | null export let adoptTemplate: (node: Node, template: string) => Node | null
export let locateHydrationNode: () => void export let locateHydrationNode: (isFragment?: boolean) => void
type Anchor = Comment & { type Anchor = Comment & {
// cached matching fragment start to avoid repeated traversal // cached matching fragment start to avoid repeated traversal
@ -82,7 +83,7 @@ function adoptTemplateImpl(node: Node, template: string): Node | null {
return node return node
} }
function locateHydrationNodeImpl() { function locateHydrationNodeImpl(isFragment?: boolean) {
let node: Node | null let node: Node | null
// prepend / firstChild // prepend / firstChild
if (insertionAnchor === 0) { if (insertionAnchor === 0) {
@ -92,6 +93,14 @@ function locateHydrationNodeImpl() {
node = insertionAnchor node = insertionAnchor
} else { } else {
node = insertionParent ? insertionParent.lastChild : currentHydrationNode node = insertionParent ? insertionParent.lastChild : currentHydrationNode
// if the last child is a comment, it is the anchor for the fragment
// so it need to find the previous node
if (isFragment && node && isDynamicFragmentEndAnchor(node)) {
let previous = prev(node)
if (previous) node = previous
}
if (node && isComment(node, ']')) { if (node && isComment(node, ']')) {
// fragment backward search // fragment backward search
if (node.$fs) { if (node.$fs) {
@ -157,3 +166,25 @@ export function locateEndAnchor(
} }
return null return null
} }
export function locateStartAnchor(
node: Node | null,
open = '[',
close = ']',
): Node | null {
let match = 0
while (node) {
if (node.nodeType === 8) {
if ((node as Comment).data === close) match++
if ((node as Comment).data === open) {
if (match === 0) {
return node
} else {
match--
}
}
}
node = node.previousSibling
}
return null
}

View File

@ -1,5 +1,10 @@
import { isDynamicAnchor } from '@vue/runtime-dom' import { isDynamicAnchor } from '@vue/runtime-dom'
import { isComment, isEmptyText, locateEndAnchor } from './hydration' import {
isComment,
isEmptyText,
locateEndAnchor,
locateStartAnchor,
} from './hydration'
/*! #__NO_SIDE_EFFECTS__ */ /*! #__NO_SIDE_EFFECTS__ */
export function createTextNode(value = ''): Text { export function createTextNode(value = ''): Text {
@ -43,21 +48,17 @@ function _next(node: Node): Node {
/*! #__NO_SIDE_EFFECTS__ */ /*! #__NO_SIDE_EFFECTS__ */
function __next(node: Node): Node { function __next(node: Node): Node {
// treat dynamic node (<!--[[-->...<!--]]-->) as a single node // treat dynamic node (<!--[[-->...<!--]]-->) as a single node
if (node && isComment(node, '[[')) { if (isComment(node, '[[')) {
node = locateEndAnchor(node, '[[', ']]')! node = locateEndAnchor(node, '[[', ']]')!
} }
// treat dynamic node (<!--[-->...<!--]-->) as a single node // treat dynamic node (<!--[-->...<!--]-->) as a single node
else if (node && isComment(node, '[')) { else if (isComment(node, '[')) {
node = locateEndAnchor(node)! node = locateEndAnchor(node)!
} }
let n = node.nextSibling! let n = node.nextSibling!
// skip if: while (n && isNonHydrationNode(n)) {
// - dynamic anchors (<!--[[-->, <!--]]-->)
// - fragment end anchor (`<!--]-->`)
// - empty text nodes
while (n && (isDynamicAnchor(n) || isComment(n, ']') || isEmptyText(n))) {
n = n.nextSibling! n = n.nextSibling!
} }
return n return n
@ -105,3 +106,48 @@ export function disableHydrationNodeLookup(): void {
next.impl = _next next.impl = _next
nthChild.impl = _nthChild nthChild.impl = _nthChild
} }
/*! #__NO_SIDE_EFFECTS__ */
export function prev(node: Node): Node | null {
// treat dynamic node (<!--[[-->...<!--]]-->) as a single node
if (isComment(node, ']]')) {
node = locateStartAnchor(node, '[[', ']]')!
}
// treat dynamic node (<!--[-->...<!--]-->) as a single node
else if (isComment(node, ']')) {
node = locateStartAnchor(node)!
}
let n = node.previousSibling
while (n && isNonHydrationNode(n)) {
n = n.previousSibling
}
return n
}
function isNonHydrationNode(node: Node) {
return (
// empty text nodes, no need to hydrate
isEmptyText(node) ||
// dynamic anchors (<!--[[-->, <!--]]-->)
isDynamicAnchor(node) ||
// fragment end anchor (`<!--]-->`)
isComment(node, ']') ||
isDynamicFragmentAnchor(node)
)
}
function isDynamicFragmentAnchor(node: Node) {
return __DEV__
? // v-if anchor (`<!--if-->`)
isComment(node, 'if') ||
// v-for anchor (`<!--for-->`)
isComment(node, 'for') ||
// v-slot anchor (`<!--slot-->`)
isComment(node, 'slot') ||
// dynamic-component anchor (`<!--dynamic-component-->`)
isComment(node, 'dynamic-component')
: // TODO ?
isComment(node, '$')
}