`)
})
})
+
+ describe('dynamic child anchor', () => {
+ test('component with element siblings', () => {
+ expect(
+ getCompiledString(`
+ <div>
+ <div/>
+ <Comp1/>
+ <div/>
+ </div>
+ `),
+ ).toMatchInlineSnapshot(`
+ "\`<div><div></div>\`)
+ _push("<!--[[-->")
+ _push(_ssrRenderComponent(_component_Comp1, null, null, _parent))
+ _push("<!--]]-->")
+ _push(\`<div></div></div>\`"
+ `)
+ })
+
+ test('with consecutive components', () => {
+ expect(
+ getCompiledString(`
+ <div>
+ <div/>
+ <Comp1/>
+ <Comp2/>
+ <div/>
+ </div>
+ `),
+ ).toMatchInlineSnapshot(`
+ "\`<div><div></div>\`)
+ _push("<!--[[-->")
+ _push(_ssrRenderComponent(_component_Comp1, null, null, _parent))
+ _push("<!--]]-->")
+ _push("<!--[[-->")
+ _push(_ssrRenderComponent(_component_Comp2, null, null, _parent))
+ _push("<!--]]-->")
+ _push(\`<div></div></div>\`"
+ `)
+ })
+ })
})
node.ssrCodegenNode.arguments.push(`_scopeId`)
}
+ // `<!--[[-->` marks the start of the dynamic children
+ // Only used in Vapor hydration, VDOM hydration
+ // skips this marker.
+ const needDynamicAnchor = shouldAddDynamicAnchor(parent, node)
+ if (needDynamicAnchor) {
+ context.pushStatement(createCallExpression(`_push`, [`"<!--[[-->"`]))
+ }
if (typeof component === 'string') {
// static component
context.pushStatement(
// the codegen node is a `renderVNode` call
context.pushStatement(node.ssrCodegenNode)
}
+ if (needDynamicAnchor) {
+ context.pushStatement(createCallExpression(`_push`, [`"<!--]]-->"`]))
+ }
}
}
return v
}
}
+
+function shouldAddDynamicAnchor(
+ parent: { tag?: string; children: TemplateChildNode[] },
+ node: TemplateChildNode,
+): boolean {
+ if (!parent.tag) return false
+
+ const children = parent.children
+ const len = children.length
+ const index = children.indexOf(node)
+
+ const isStaticElement = (c: TemplateChildNode): boolean =>
+ c.type === NodeTypes.ELEMENT && c.tagType !== ElementTypes.COMPONENT
+
+ let hasStaticPreviousSibling = false
+ if (index > 0) {
+ for (let i = index - 1; i >= 0; i--) {
+ if (isStaticElement(children[i])) {
+ hasStaticPreviousSibling = true
+ break
+ }
+ }
+ }
+
+ let hasStaticNextSibling = false
+ if (hasStaticPreviousSibling && index > -1 && index < len - 1) {
+ for (let i = index + 1; i < len; i++) {
+ if (isStaticElement(children[i])) {
+ hasStaticNextSibling = true
+ break
+ }
+ }
+ }
+
+ return hasStaticPreviousSibling && hasStaticNextSibling
+}
}
})
+ describe('dynamic child anchor', () => {
+ test('component with element siblings', () => {
+ const Comp = {
+ render() {
+ return createTextVNode('foo')
+ },
+ }
+ const { vnode, container } = mountWithHydration(
+ `<div><span></span><!--[[-->foo<!--]]--><span></span></div>`,
+ () => h('div', null, [h('span'), h(Comp), h('span')]),
+ )
+ expect(vnode.el).toBe(container.firstChild)
+ expect(`Hydration children mismatch`).not.toHaveBeenWarned()
+ })
+
+ test('with consecutive components', () => {
+ const Comp = {
+ render() {
+ return createTextVNode('foo')
+ },
+ }
+ const { vnode, container } = mountWithHydration(
+ `<div><span></span><!--[[-->foo<!--]]--><!--[[-->foo<!--]]--><span></span></div>`,
+ () => h('div', null, [h('span'), h(Comp), h(Comp), h('span')]),
+ )
+ expect(vnode.el).toBe(container.firstChild)
+ expect(`Hydration children mismatch`).not.toHaveBeenWarned()
+ })
+ })
+
describe('mismatch handling', () => {
test('text node', () => {
const { container } = mountWithHydration(`foo`, () => 'bar')
o: {
patchProp,
createText,
- nextSibling,
+ nextSibling: next,
parentNode,
remove,
insert,
},
} = rendererInternals
+ function isDynamicAnchor(node: Node): boolean {
+ return isComment(node) && (node.data === '[[' || node.data === ']]')
+ }
+
+ function nextSibling(node: Node) {
+ let n = next(node)
+ // skip dynamic child anchor
+ if (n && isDynamicAnchor(n)) {
+ n = next(n)
+ }
+ return n
+ }
+
const hydrate: RootHydrateFunction = (vnode, container) => {
if (!container.hasChildNodes()) {
;(__DEV__ || __FEATURE_PROD_HYDRATION_MISMATCH_DETAILS__) &&
slotScopeIds: string[] | null,
optimized = false,
): Node | null => {
+ if (isDynamicAnchor(node)) node = nextSibling(node)!
optimized = optimized || !!vnode.dynamicChildren
const isFragmentStart = isComment(node) && node.data === '['
const onMismatch = () =>
// The SSRed DOM contains more nodes than it should. Remove them.
const cur = next
- next = next.nextSibling
+ next = nextSibling(next)
remove(cur)
}
} else if (shapeFlag & ShapeFlags.TEXT_CHILDREN) {
}
}
- return el.nextSibling
+ return nextSibling(el)
}
const hydrateChildren = (
)
})
- // problem is the <!> placeholder does not exist in SSR output
- test.todo('component with anchor insertion', async () => {
+ test('component with anchor insertion', async () => {
const { container, data } = await testHydration(
`
<template>
Child: `<template>{{ data }}</template>`,
},
)
- expect(container.innerHTML).toMatchInlineSnapshot()
+ expect(container.innerHTML).toMatchInlineSnapshot(
+ `"<div><span></span><!--[[-->foo<!--]]--><span></span></div>"`,
+ )
data.value = 'bar'
await nextTick()
- expect(container.innerHTML).toMatchInlineSnapshot()
+ expect(container.innerHTML).toMatchInlineSnapshot(
+ `"<div><span></span><!--[[-->bar<!--]]--><span></span></div>"`,
+ )
})
- test.todo('consecutive component with anchor insertion', async () => {
+ test('consecutive component with anchor insertion', async () => {
const { container, data } = await testHydration(
`<template>
<div>
Child: `<template>{{ data }}</template>`,
},
)
- expect(container.innerHTML).toMatchInlineSnapshot()
+ expect(container.innerHTML).toMatchInlineSnapshot(
+ `"<div><span></span><!--[[-->foo<!--]]--><!--[[-->foo<!--]]--><span></span></div>"`,
+ )
data.value = 'bar'
await nextTick()
- expect(container.innerHTML).toMatchInlineSnapshot()
+ expect(container.innerHTML).toMatchInlineSnapshot(
+ `"<div><span></span><!--[[-->bar<!--]]--><!--[[-->bar<!--]]--><span></span></div>"`,
+ )
})
test.todo('if')
import { warn } from '@vue/runtime-dom'
import {
+ type Anchor,
insertionAnchor,
insertionParent,
resetInsertionState,
export let adoptTemplate: (node: Node, template: string) => Node | null
export let locateHydrationNode: () => void
-type Anchor = Comment & {
- // cached matching fragment start to avoid repeated traversal
- // on nested fragments
- $fs?: Anchor
-}
-
const isComment = (node: Node, data: string): node is Anchor =>
node.nodeType === 8 && (node as Comment).data === data
function locateHydrationNodeImpl() {
let node: Node | null
-
// prepend / firstChild
if (insertionAnchor === 0) {
node = child(insertionParent!)
} else {
- node = insertionAnchor
- ? insertionAnchor.previousSibling
- : insertionParent
- ? insertionParent.lastChild
- : currentHydrationNode
-
- if (node && isComment(node, ']')) {
- // fragment backward search
- if (node.$fs) {
- // already cached matching fragment start
- node = node.$fs
- } else {
- let cur: Node | null = node
- let curFragEnd = node
- let fragDepth = 0
- node = null
- while (cur) {
- cur = cur.previousSibling
- if (cur) {
- if (isComment(cur, '[')) {
- curFragEnd.$fs = cur
- if (!fragDepth) {
- node = cur
- break
- } else {
- fragDepth--
+ // dynamic child anchor `<!--[[-->`
+ if (insertionAnchor && isDynamicStart(insertionAnchor)) {
+ const anchor = (insertionParent!.lds = insertionParent!.lds
+ ? // continuous dynamic children, the next dynamic start must exist
+ locateNextDynamicStart(insertionParent!.lds)!
+ : insertionAnchor)
+ node = anchor.nextSibling
+ } else {
+ node = insertionAnchor
+ ? insertionAnchor.previousSibling
+ : insertionParent
+ ? insertionParent.lastChild
+ : currentHydrationNode
+ if (node && isComment(node, ']')) {
+ // fragment backward search
+ if (node.$fs) {
+ // already cached matching fragment start
+ node = node.$fs
+ } else {
+ let cur: Node | null = node
+ let curFragEnd = node
+ let fragDepth = 0
+ node = null
+ while (cur) {
+ cur = cur.previousSibling
+ if (cur) {
+ if (isComment(cur, '[')) {
+ curFragEnd.$fs = cur
+ if (!fragDepth) {
+ node = cur
+ break
+ } else {
+ fragDepth--
+ }
+ } else if (isComment(cur, ']')) {
+ curFragEnd = cur
+ fragDepth++
}
- } else if (isComment(cur, ']')) {
- curFragEnd = cur
- fragDepth++
}
}
}
resetInsertionState()
currentHydrationNode = node
}
+
+function isDynamicStart(node: Node): node is Anchor {
+ return isComment(node, '[[')
+}
+
+function locateNextDynamicStart(anchor: Anchor): Anchor | undefined {
+ let cur: Node | null = anchor
+ let end = null
+ let depth = 0
+ while (cur) {
+ cur = cur.nextSibling
+ if (cur) {
+ if (isComment(cur, '[[')) {
+ depth++
+ } else if (isComment(cur, ']]')) {
+ if (!depth) {
+ end = cur
+ break
+ } else {
+ depth--
+ }
+ }
+ }
+ }
+
+ if (end) {
+ return end!.nextSibling as Anchor
+ }
+}
-export let insertionParent: ParentNode | undefined
-export let insertionAnchor: Node | 0 | undefined
+export let insertionParent:
+ | (ParentNode & {
+ // cached the last dynamic start anchor
+ lds?: Anchor
+ })
+ | undefined
+export let insertionAnchor: Node | 0 | undefined | null
/**
* This function is called before a block type that requires insertion
export function resetInsertionState(): void {
insertionParent = insertionAnchor = undefined
}
+
+export function setInsertionAnchor(anchor: Node | null): void {
+ insertionAnchor = anchor
+}
+
+export type Anchor = Comment & {
+ // cached matching fragment start to avoid repeated traversal
+ // on nested fragments
+ $fs?: Anchor
+}