context: CodegenContext,
): CodeFragment[] {
const [frag, push] = buildCodeFragment()
- const { id, template, operation } = dynamic
+ const { id, template, operation, dynamicChildOffset } = dynamic
if (id !== undefined && template !== undefined) {
- push(NEWLINE, `const n${id} = t${template}()`)
+ push(NEWLINE, `const n${id} = t${template}(${dynamicChildOffset || ''})`)
push(...genDirectivesForElement(id, context))
}
children: IRDynamicInfo[]
template?: number
hasDynamicChild?: boolean
+ dynamicChildOffset?: number
operation?: OperationNode
}
function processDynamicChildren(context: TransformContext<ElementNode>) {
let prevDynamics: IRDynamicInfo[] = []
- let hasStaticTemplate = false
+ let staticCount = 0
const children = context.dynamic.children
for (const [index, child] of children.entries()) {
if (!(child.flags & DynamicFlag.NON_TEMPLATE)) {
if (prevDynamics.length) {
- if (hasStaticTemplate) {
+ if (staticCount) {
// each dynamic child gets its own placeholder node.
// this makes it easier to locate the corresponding node during hydration.
for (let i = 0; i < prevDynamics.length; i++) {
}
prevDynamics = []
}
- hasStaticTemplate = true
+ staticCount++
}
}
if (prevDynamics.length) {
registerInsertion(prevDynamics, context)
+ context.dynamic.dynamicChildOffset = staticCount
}
}
move(vnode: VNode, container: any, anchor: any): void
slot(n1: VNode | null, n2: VNode, container: any, anchor: any): void
hydrate(node: Node, fn: () => void): void
+ hydrateSlot(vnode: VNode, container: any): void
vdomMount: (component: ConcreteComponent, props?: any, slots?: any) => any
vdomUnmount: UnmountComponentFn
Comment as VComment,
type VNode,
type VNodeHook,
+ VaporSlot,
createTextVNode,
createVNode,
invokeVNodeHook,
)
}
break
+ case VaporSlot:
+ getVaporInterface(parentComponent, vnode).hydrateSlot(
+ vnode,
+ parentNode(node)!,
+ )
+ break
default:
if (shapeFlag & ShapeFlags.ELEMENT) {
if (
)
})
+ test('consecutive components with insertion parent', async () => {
+ const data = reactive({ foo: 'foo', bar: 'bar' })
+ const { container } = await testHydration(
+ `<template>
+ <div>
+ <components.Child1/>
+ <components.Child2/>
+ </div>
+ </template>
+ `,
+ {
+ Child1: `<template><span>{{ data.foo }}</span></template>`,
+ Child2: `<template><span>{{ data.bar }}</span></template>`,
+ },
+ data,
+ )
+ expect(container.innerHTML).toBe(
+ `<div><span>foo</span><span>bar</span></div>`,
+ )
+
+ data.foo = 'foo1'
+ data.bar = 'bar1'
+ await nextTick()
+ expect(container.innerHTML).toBe(
+ `<div><span>foo1</span><span>bar1</span></div>`,
+ )
+ })
+
test('nested consecutive components with anchor insertion', async () => {
const { container, data } = await testHydration(
`
`</div>`,
)
})
+
+ test('dynamic component fallback', async () => {
+ const { container, data } = await testHydration(
+ `<template>
+ <component :is="'button'">
+ <span>{{ data }}</span>
+ </component>
+ </template>`,
+ {},
+ ref('foo'),
+ )
+
+ expect(container.innerHTML).toBe(
+ `<button><span>foo</span></button><!--${anchorLabel}-->`,
+ )
+ data.value = 'bar'
+ await nextTick()
+ expect(container.innerHTML).toBe(
+ `<button><span>bar</span></button><!--${anchorLabel}-->`,
+ )
+ })
})
describe('if', () => {
)
})
+ test('consecutive component with insertion parent', async () => {
+ const data = reactive({
+ show: true,
+ foo: 'foo',
+ bar: 'bar',
+ })
+ const { container } = await testHydration(
+ `<template>
+ <div v-if="data.show">
+ <components.Child/>
+ <components.Child2/>
+ </div>
+ </template>`,
+ {
+ Child: `<template><span>{{data.foo}}</span></template>`,
+ Child2: `<template><span>{{data.bar}}</span></template>`,
+ },
+ data,
+ )
+ expect(container.innerHTML).toBe(
+ `<div>` +
+ `<span>foo</span>` +
+ `<span>bar</span>` +
+ `</div>` +
+ `<!--${anchorLabel}-->`,
+ )
+
+ data.show = false
+ await nextTick()
+ expect(container.innerHTML).toBe(`<!--${anchorLabel}-->`)
+ })
+
test('consecutive v-if on component with anchor insertion', async () => {
const data = ref(true)
const { container } = await testHydration(
`</div>`,
)
})
+
+ test('slot fallback', async () => {
+ const data = reactive({
+ foo: 'foo',
+ })
+ const { container } = await testHydration(
+ `<template>
+ <components.Child>
+ </components.Child>
+ </template>`,
+ {
+ Child: `<template><slot><span>{{data.foo}}</span></slot></template>`,
+ },
+ data,
+ )
+ expect(container.innerHTML).toBe(
+ `<!--[--><span>foo</span><!--]--><!--${slotAnchorLabel}-->`,
+ )
+
+ data.foo = 'bar'
+ await nextTick()
+ expect(container.innerHTML).toBe(
+ `<!--[--><span>bar</span><!--]--><!--${slotAnchorLabel}-->`,
+ )
+ })
})
describe.todo('transition', async () => {
expect(container.innerHTML).toMatchInlineSnapshot(`"false"`)
})
+ test('nested components (VDOM -> Vapor -> VDOM (with slot fallback))', async () => {
+ const data = ref(true)
+ const { container } = await testHydrationInterop(
+ `<script setup>const data = _data; const components = _components;</script>
+ <template>
+ <components.VaporChild/>
+ </template>`,
+ {
+ VaporChild: {
+ code: `<template><components.VdomChild/></template>`,
+ vapor: true,
+ },
+ VdomChild: {
+ code: `<script setup>const data = _data;</script>
+ <template><slot><span>{{data}}</span></slot></template>`,
+ vapor: false,
+ },
+ },
+ data,
+ )
+
+ expect(container.innerHTML).toMatchInlineSnapshot(
+ `"<!--[--><span>true</span><!--]--><!--slot-->"`,
+ )
+
+ data.value = false
+ await nextTick()
+ expect(container.innerHTML).toMatchInlineSnapshot(
+ `"<!--[--><span>false</span><!--]--><!--slot-->"`,
+ )
+ })
+
+ test('nested components (VDOM -> Vapor(with slot fallback) -> VDOM)', async () => {
+ const data = ref(true)
+ const { container } = await testHydrationInterop(
+ `<script setup>const data = _data; const components = _components;</script>
+ <template>
+ <components.VaporChild/>
+ </template>`,
+ {
+ VaporChild: {
+ code: `<template>
+ <components.VdomChild>
+ <template #default>
+ <span>{{data}} vapor fallback</span>
+ </template>
+ </components.VdomChild>
+ </template>`,
+ vapor: true,
+ },
+ VdomChild: {
+ code: `<script setup>const data = _data;</script>
+ <template><slot><span>vdom fallback</span></slot></template>`,
+ vapor: false,
+ },
+ },
+ data,
+ )
+
+ expect(container.innerHTML).toMatchInlineSnapshot(
+ `"<!--[--><span>true vapor fallback</span><!--]--><!--slot-->"`,
+ )
+
+ data.value = false
+ await nextTick()
+ expect(container.innerHTML).toMatchInlineSnapshot(
+ `"<!--[--><span>false vapor fallback</span><!--]--><!--slot-->"`,
+ )
+ })
+
test('vapor slot render vdom component', async () => {
const data = ref(true)
const { container } = await testHydrationInterop(
const _insertionParent = insertionParent
const _insertionAnchor = insertionAnchor
if (isHydrating) {
- locateHydrationNode(true)
+ locateHydrationNode()
} else {
resetInsertionState()
}
constructor(anchorLabel?: string) {
super([])
if (isHydrating) {
- locateHydrationNode(true)
+ locateHydrationNode()
this.hydrate(anchorLabel!)
} else {
this.anchor =
getSlot,
} from './componentSlots'
import { hmrReload, hmrRerender } from './hmr'
-import { isHydrating, locateHydrationNode } from './dom/hydration'
+import {
+ adoptTemplate,
+ currentHydrationNode,
+ isHydrating,
+ locateHydrationNode,
+ setCurrentHydrationNode,
+} from './dom/hydration'
import {
insertionAnchor,
insertionParent,
resetInsertionState()
}
- const el = document.createElement(comp)
+ const el = isHydrating
+ ? (adoptTemplate(currentHydrationNode!, `<${comp}/>`) as HTMLElement)
+ : document.createElement(comp)
// mark single root
;(el as any).$root = isSingleRoot
}
if (rawSlots) {
+ isHydrating && setCurrentHydrationNode(el.firstChild)
if (rawSlots.$) {
// TODO dynamic slot fragment
} else {
setInsertionState,
} from '../insertionState'
import {
+ _nthChild,
disableHydrationNodeLookup,
enableHydrationNodeLookup,
- next,
} from './node'
-import { isVaporAnchors, isVaporFragmentAnchor } from '@vue/shared'
+import { isVaporAnchors } from '@vue/shared'
export let isHydrating = false
export let currentHydrationNode: Node | null = null
locateHydrationNode = locateHydrationNodeImpl
// optimize anchor cache lookup
- ;(Comment.prototype as any).$fs = undefined
+ ;(Comment.prototype as any).$fe = undefined
+ ;(Node.prototype as any).$dp = undefined
isOptimized = true
}
enableHydrationNodeLookup()
}
export let adoptTemplate: (node: Node, template: string) => Node | null
-export let locateHydrationNode: (hasFragmentAnchor?: boolean) => void
+export let locateHydrationNode: () => void
type Anchor = Comment & {
- // cached matching fragment start to avoid repeated traversal
+ // cached matching fragment end to avoid repeated traversal
// on nested fragments
- $fs?: Anchor
+ $fe?: Anchor
}
export const isComment = (node: Node, data: string): node is Anchor =>
}
}
- currentHydrationNode = next(node)
+ currentHydrationNode = node.nextSibling
return node
}
-const hydrationPositionMap = new WeakMap<ParentNode, Node>()
-
-function locateHydrationNodeImpl(hasFragmentAnchor?: boolean) {
+function locateHydrationNodeImpl() {
let node: Node | null
// prepend / firstChild
if (insertionAnchor === 0) {
// SSR Output: `...<span/>Content<span/>...`// `insertionAnchor` is the actual node
node = insertionAnchor
} else {
- node = insertionParent
- ? hydrationPositionMap.get(insertionParent) || insertionParent.lastChild
- : currentHydrationNode
-
- // if node is a vapor fragment anchor, find the previous one
- if (hasFragmentAnchor && node && isVaporFragmentAnchor(node)) {
- node = node.previousSibling
- if (__DEV__ && !node) {
- // this should not happen
- throw new Error(`vapor fragment anchor previous node was not found.`)
- }
- }
+ node = 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++
- }
- }
- }
- }
+ // if current hydration node is not under the current parent, or no
+ // current node, find node by dynamic position or use the first child
+ if (insertionParent && (!node || node.parentNode !== insertionParent)) {
+ node = _nthChild(insertionParent, insertionParent.$dp || 0)
}
- if (insertionParent && node) {
- const prev = node.previousSibling
- if (prev) hydrationPositionMap.set(insertionParent, prev)
+ while (node && isNonHydrationNode(node)) {
+ node = node.nextSibling!
}
}
}
export function locateEndAnchor(
- node: Node | null,
+ node: Anchor,
open = '[',
close = ']',
): Node | null {
- let match = 0
- while (node) {
- node = node.nextSibling
- if (node && node.nodeType === 8) {
- if ((node as Comment).data === open) match++
- if ((node as Comment).data === close) {
- if (match === 0) {
- return node
- } else {
- match--
- }
+ // already cached matching end
+ if (node.$fe) {
+ return node.$fe
+ }
+
+ const stack: Anchor[] = [node]
+ while ((node = node.nextSibling as Anchor) && stack.length > 0) {
+ if (node.nodeType === 8) {
+ if (node.data === open) {
+ stack.push(node)
+ } else if (node.data === close) {
+ const matchingOpen = stack.pop()!
+ matchingOpen.$fe = node
+ if (stack.length === 0) return node
}
}
}
+
return null
}
/*! #__NO_SIDE_EFFECTS__ */
export function template(html: string, root?: boolean) {
let node: Node
- return (): Node & { $root?: true } => {
+ return (n?: number): Node & { $root?: true } => {
if (isHydrating) {
if (__DEV__ && !currentHydrationNode) {
// TODO this should not happen
throw new Error('No current hydration node')
}
- return adoptTemplate(currentHydrationNode!, html)!
+ node = adoptTemplate(currentHydrationNode!, html)!
+ // dynamic node position, default is 0
+ ;(node as any).$dp = n || 0
+ return node
}
// fast path for text nodes
if (html[0] !== '<') {
-export let insertionParent: ParentNode | undefined
+export let insertionParent:
+ | (ParentNode & {
+ // dynamic node position - hydration only
+ // indicates the position where dynamic nodes begin within the parent
+ // during hydration, static nodes before this index are skipped
+ //
+ // Example:
+ // const t0 = _template("<div><span></span><span></span></div>", true)
+ // const n4 = t0(2) // n4.$dp = 2
+ // The first 2 nodes are static, dynamic nodes start from index 2
+ $dp?: number
+ })
+ | undefined
export let insertionAnchor: Node | 0 | undefined
/**
currentHydrationNode,
isHydrating,
locateHydrationNode,
+ locateVaporFragmentAnchor,
+ setCurrentHydrationNode,
hydrateNode as vaporHydrateNode,
} from './dom/hydration'
},
hydrate: vaporHydrateNode,
+ hydrateSlot(vnode, container) {
+ const { slot } = vnode.vs!
+ const propsRef = (vnode.vs!.ref = shallowRef(vnode.props))
+ const slotBlock = slot(new Proxy(propsRef, vaporSlotPropsProxyHandler))
+ vaporHydrateNode(slotBlock, () => {
+ const anchor = locateVaporFragmentAnchor(currentHydrationNode!, 'slot')!
+ vnode.el = vnode.anchor = anchor
+ insert((vnode.vb = slotBlock), container, anchor)
+ })
+ },
}
const vaporSlotPropsProxyHandler: ProxyHandler<
frag.insert = (parentNode, anchor) => {
if (!isMounted || isHydrating) {
if (isHydrating) {
- ;(
- vdomHydrateNode ||
- (vdomHydrateNode = ensureHydrationRenderer().hydrateNode!)
- )(
- currentHydrationNode!,
- vnode,
- parentInstance as any,
- null,
- null,
- false,
- )
+ hydrateVNode(vnode, parentInstance as any)
} else {
internals.mt(
vnode,
props,
)
if (isHydrating) {
- locateHydrationNode(true)
- ;(
- vdomHydrateNode ||
- (vdomHydrateNode = ensureHydrationRenderer().hydrateNode!)
- )(
- currentHydrationNode!,
- vnode,
- parentComponent as any,
- null,
- null,
- false,
- )
+ hydrateVNode(vnode!, parentComponent as any)
} else {
if ((vnode.children as any[]).length) {
if (fallbackNodes) {
return frag
}
+function hydrateVNode(
+ vnode: VNode,
+ parentComponent: ComponentInternalInstance | null,
+) {
+ // keep fragment start anchor, hydrateNode uses it to
+ // determine if node is a fragmentStart
+ locateHydrationNode()
+ if (!vdomHydrateNode) vdomHydrateNode = ensureHydrationRenderer().hydrateNode!
+ const nextNode = vdomHydrateNode(
+ currentHydrationNode!,
+ vnode,
+ parentComponent,
+ null,
+ null,
+ false,
+ )
+ setCurrentHydrationNode(nextNode)
+}
+
export const vaporInteropPlugin: Plugin = app => {
const internals = ensureRenderer().internals
app._context.vapor = extend(vaporInteropImpl, {