mirror of https://github.com/vuejs/core.git
wip: hydration
This commit is contained in:
parent
bba71becba
commit
196d551437
|
@ -24,10 +24,10 @@ export function genSelf(
|
|||
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))
|
||||
}
|
||||
|
||||
|
|
|
@ -266,6 +266,7 @@ export interface IRDynamicInfo {
|
|||
children: IRDynamicInfo[]
|
||||
template?: number
|
||||
hasDynamicChild?: boolean
|
||||
dynamicChildOffset?: number
|
||||
operation?: OperationNode
|
||||
needsKey?: boolean
|
||||
}
|
||||
|
|
|
@ -59,7 +59,7 @@ export const transformChildren: NodeTransform = (node, context) => {
|
|||
|
||||
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()) {
|
||||
|
@ -69,7 +69,7 @@ function processDynamicChildren(context: TransformContext<ElementNode>) {
|
|||
|
||||
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++) {
|
||||
|
@ -92,12 +92,13 @@ function processDynamicChildren(context: TransformContext<ElementNode>) {
|
|||
}
|
||||
prevDynamics = []
|
||||
}
|
||||
hasStaticTemplate = true
|
||||
staticCount++
|
||||
}
|
||||
}
|
||||
|
||||
if (prevDynamics.length) {
|
||||
registerInsertion(prevDynamics, context)
|
||||
registerInsertion(prevDynamics, context, undefined)
|
||||
context.dynamic.dynamicChildOffset = staticCount
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -476,6 +476,34 @@ describe('Vapor Mode hydration', () => {
|
|||
)
|
||||
})
|
||||
|
||||
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(
|
||||
`
|
||||
|
@ -1314,6 +1342,38 @@ describe('Vapor Mode hydration', () => {
|
|||
)
|
||||
})
|
||||
|
||||
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(
|
||||
|
@ -2354,6 +2414,31 @@ describe('Vapor Mode hydration', () => {
|
|||
`</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 () => {
|
||||
|
@ -3912,6 +3997,79 @@ describe('VDOM hydration interop', () => {
|
|||
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.todo(
|
||||
'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(
|
||||
|
|
|
@ -86,7 +86,7 @@ export const createFor = (
|
|||
const _insertionParent = insertionParent
|
||||
const _insertionAnchor = insertionAnchor
|
||||
if (isHydrating) {
|
||||
locateHydrationNode(true)
|
||||
locateHydrationNode()
|
||||
} else {
|
||||
resetInsertionState()
|
||||
}
|
||||
|
|
|
@ -6,11 +6,12 @@ import {
|
|||
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
|
||||
|
@ -29,9 +30,9 @@ function performHydration<T>(
|
|||
if (!isOptimized) {
|
||||
adoptTemplate = adoptTemplateImpl
|
||||
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()
|
||||
|
@ -58,12 +59,20 @@ export function hydrateNode(node: Node, fn: () => void): void {
|
|||
}
|
||||
|
||||
export let adoptTemplate: (node: Node, template: string) => Node | null
|
||||
export let locateHydrationNode: (hasFragmentAnchor?: boolean) => void
|
||||
export let locateHydrationNode: () => void
|
||||
|
||||
let inVNodeHydration = 0
|
||||
export function setInVNodeHydration(): void {
|
||||
inVNodeHydration++
|
||||
}
|
||||
export function unsetInVNodeHydration(): void {
|
||||
inVNodeHydration--
|
||||
}
|
||||
|
||||
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 =>
|
||||
|
@ -99,9 +108,9 @@ function adoptTemplateImpl(node: Node, template: string): Node | null {
|
|||
return node
|
||||
}
|
||||
|
||||
const hydrationPositionMap = new WeakMap<ParentNode, Node>()
|
||||
const nextHydrationNodeMap = new WeakMap<ParentNode, Node>()
|
||||
|
||||
function locateHydrationNodeImpl(hasFragmentAnchor?: boolean) {
|
||||
function locateHydrationNodeImpl() {
|
||||
let node: Node | null
|
||||
// prepend / firstChild
|
||||
if (insertionAnchor === 0) {
|
||||
|
@ -113,51 +122,31 @@ function locateHydrationNodeImpl(hasFragmentAnchor?: boolean) {
|
|||
node = insertionAnchor
|
||||
} else {
|
||||
node = insertionParent
|
||||
? hydrationPositionMap.get(insertionParent) || insertionParent.lastChild
|
||||
? nextHydrationNodeMap.get(insertionParent) ||
|
||||
(insertionParent.$dp !== undefined
|
||||
? _nthChild(insertionParent, insertionParent.$dp)
|
||||
: currentHydrationNode)
|
||||
: 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.`)
|
||||
}
|
||||
}
|
||||
|
||||
if (node && isComment(node, ']')) {
|
||||
// fragment backward search
|
||||
if (node.$fs) {
|
||||
// already cached matching fragment start
|
||||
node = node.$fs
|
||||
let nextNode: Node | null = null
|
||||
while (node) {
|
||||
const isFragStart = isComment(node, '[')
|
||||
if (isFragStart)
|
||||
nextNode = locateEndAnchor(node as Anchor, '[', ']')!.nextSibling
|
||||
if (
|
||||
isNonHydrationNode(node) ||
|
||||
// don't skip fragment start for vnode hydration
|
||||
(!inVNodeHydration && isFragStart)
|
||||
) {
|
||||
node = node.nextSibling!
|
||||
} 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++
|
||||
}
|
||||
}
|
||||
}
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
if (insertionParent && node) {
|
||||
const prev = node.previousSibling
|
||||
if (prev) hydrationPositionMap.set(insertionParent, prev)
|
||||
const next = nextNode || node.nextSibling
|
||||
if (next) nextHydrationNodeMap.set(insertionParent, next)
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -171,24 +160,28 @@ function locateHydrationNodeImpl(hasFragmentAnchor?: boolean) {
|
|||
}
|
||||
|
||||
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
|
||||
}
|
||||
|
||||
|
|
|
@ -6,13 +6,16 @@ let t: HTMLTemplateElement
|
|||
/*! #__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] !== '<') {
|
||||
|
|
|
@ -60,7 +60,7 @@ export class DynamicFragment extends VaporFragment {
|
|||
constructor(anchorLabel?: string) {
|
||||
super([])
|
||||
if (isHydrating) {
|
||||
locateHydrationNode(true)
|
||||
locateHydrationNode()
|
||||
this.hydrate(anchorLabel!)
|
||||
} else {
|
||||
this.anchor =
|
||||
|
|
|
@ -1,4 +1,16 @@
|
|||
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
|
||||
|
||||
/**
|
||||
|
|
|
@ -55,6 +55,9 @@ import {
|
|||
currentHydrationNode,
|
||||
isHydrating,
|
||||
locateHydrationNode,
|
||||
setCurrentHydrationNode,
|
||||
setInVNodeHydration,
|
||||
unsetInVNodeHydration,
|
||||
hydrateNode as vaporHydrateNode,
|
||||
} from './dom/hydration'
|
||||
import { DynamicFragment, VaporFragment, isFragment } from './fragment'
|
||||
|
@ -170,7 +173,11 @@ const vaporInteropImpl: Omit<
|
|||
setVaporTransitionHooks(component as any, hooks as VaporTransitionHooks)
|
||||
},
|
||||
|
||||
hydrate: vaporHydrateNode,
|
||||
hydrate(node: Node, fn: () => void) {
|
||||
setInVNodeHydration()
|
||||
vaporHydrateNode(node, fn)
|
||||
unsetInVNodeHydration()
|
||||
},
|
||||
}
|
||||
|
||||
const vaporSlotPropsProxyHandler: ProxyHandler<
|
||||
|
@ -261,17 +268,7 @@ function createVDOMComponent(
|
|||
if (transition) setVNodeTransitionHooks(vnode, transition)
|
||||
|
||||
if (isHydrating) {
|
||||
;(
|
||||
vdomHydrateNode ||
|
||||
(vdomHydrateNode = ensureHydrationRenderer().hydrateNode!)
|
||||
)(
|
||||
currentHydrationNode!,
|
||||
vnode,
|
||||
parentInstance as any,
|
||||
null,
|
||||
null,
|
||||
false,
|
||||
)
|
||||
hydrateVNode(vnode, parentInstance as any)
|
||||
} else {
|
||||
internals.mt(
|
||||
vnode,
|
||||
|
@ -344,42 +341,33 @@ function renderVDOMSlot(
|
|||
ensureVaporSlotFallback(children, fallback as any)
|
||||
isValidSlot = children.length > 0
|
||||
}
|
||||
|
||||
if (isValidSlot) {
|
||||
if (isHydrating) {
|
||||
locateHydrationNode(true)
|
||||
;(
|
||||
vdomHydrateNode ||
|
||||
(vdomHydrateNode = ensureHydrationRenderer().hydrateNode!)
|
||||
)(
|
||||
currentHydrationNode!,
|
||||
hydrateVNode(vnode!, parentComponent as any)
|
||||
} else {
|
||||
if (fallbackNodes) {
|
||||
remove(fallbackNodes, parentNode)
|
||||
fallbackNodes = undefined
|
||||
}
|
||||
internals.p(
|
||||
oldVNode,
|
||||
vnode!,
|
||||
parentNode,
|
||||
anchor,
|
||||
parentComponent as any,
|
||||
null,
|
||||
undefined,
|
||||
null,
|
||||
false,
|
||||
)
|
||||
} else if (fallbackNodes) {
|
||||
remove(fallbackNodes, parentNode)
|
||||
fallbackNodes = undefined
|
||||
}
|
||||
internals.p(
|
||||
oldVNode,
|
||||
vnode!,
|
||||
parentNode,
|
||||
anchor,
|
||||
parentComponent as any,
|
||||
null,
|
||||
undefined,
|
||||
null,
|
||||
false,
|
||||
)
|
||||
oldVNode = vnode!
|
||||
} else {
|
||||
// for forwarded slot without its own fallback, use the fallback
|
||||
// provided by the slot outlet.
|
||||
// re-fetch `frag.fallback` as it may have been updated at `createSlot`
|
||||
fallback = frag.fallback
|
||||
|
||||
if (fallback && !fallbackNodes) {
|
||||
// mount fallback
|
||||
if (oldVNode) {
|
||||
|
@ -451,7 +439,12 @@ const createFallback =
|
|||
const frag = new VaporFragment([])
|
||||
frag.insert = (parentNode, anchor) => {
|
||||
fallbackNodes.forEach(vnode => {
|
||||
internals.p(null, vnode, parentNode, anchor, parentComponent)
|
||||
// hydrate fallback
|
||||
if (isHydrating) {
|
||||
hydrateVNode(vnode, parentComponent as any)
|
||||
} else {
|
||||
internals.p(null, vnode, parentNode, anchor, parentComponent)
|
||||
}
|
||||
})
|
||||
}
|
||||
frag.remove = parentNode => {
|
||||
|
@ -465,3 +458,24 @@ const createFallback =
|
|||
// vapor slot
|
||||
return fallbackNodes as Block
|
||||
}
|
||||
|
||||
function hydrateVNode(
|
||||
vnode: VNode,
|
||||
parentComponent: ComponentInternalInstance | null,
|
||||
) {
|
||||
// keep fragment start anchor, hydrateNode uses it to
|
||||
// determine if node is a fragmentStart
|
||||
setInVNodeHydration()
|
||||
locateHydrationNode()
|
||||
if (!vdomHydrateNode) vdomHydrateNode = ensureHydrationRenderer().hydrateNode!
|
||||
const nextNode = vdomHydrateNode(
|
||||
currentHydrationNode!,
|
||||
vnode,
|
||||
parentComponent,
|
||||
null,
|
||||
null,
|
||||
false,
|
||||
)
|
||||
unsetInVNodeHydration()
|
||||
setCurrentHydrationNode(nextNode)
|
||||
}
|
||||
|
|
Loading…
Reference in New Issue