wip: hydration
ci / test (push) Has been cancelled Details
ci / continuous-release (push) Has been cancelled Details

This commit is contained in:
daiwei 2025-06-25 22:55:37 +08:00
parent bba71becba
commit 63ec52aaa9
17 changed files with 366 additions and 125 deletions

View File

@ -77,6 +77,7 @@ describe('ssr: inject <style vars>', () => {
}></div><div\${ }></div><div\${
_ssrRenderAttrs(_cssVars) _ssrRenderAttrs(_cssVars)
}></div><!--]-->\`) }></div><!--]-->\`)
_push(\`<!--if-->\`)
} }
}" }"
`) `)

View File

@ -43,6 +43,7 @@ describe('ssr: v-if', () => {
_push(\`<!--if-->\`) _push(\`<!--if-->\`)
} else { } else {
_push(\`<span\${_ssrRenderAttrs(_attrs)}></span>\`) _push(\`<span\${_ssrRenderAttrs(_attrs)}></span>\`)
_push(\`<!--if-->\`)
} }
}" }"
`) `)
@ -81,6 +82,7 @@ describe('ssr: v-if', () => {
_push(\`<!--if-->\`) _push(\`<!--if-->\`)
} else { } else {
_push(\`<p\${_ssrRenderAttrs(_attrs)}></p>\`) _push(\`<p\${_ssrRenderAttrs(_attrs)}></p>\`)
_push(\`<!--if-->\`)
} }
}" }"
`) `)
@ -170,6 +172,7 @@ describe('ssr: v-if', () => {
_push(\`<!--if-->\`) _push(\`<!--if-->\`)
} else { } else {
_push(\`<div\${_ssrRenderAttrs(_attrs)}></div>\`) _push(\`<div\${_ssrRenderAttrs(_attrs)}></div>\`)
_push(\`<!--if-->\`)
} }
}" }"
`) `)

View File

@ -80,11 +80,11 @@ function processIfBranch(
context, context,
needFragmentWrapper, needFragmentWrapper,
) )
if (branch.condition) {
// v-if/v-else-if anchor for vapor hydration // v-if/v-else-if/v-else anchor for vapor hydration
statement.body.push( statement.body.push(
createCallExpression(`_push`, [`\`<!--${IF_ANCHOR_LABEL}-->\``]), createCallExpression(`_push`, [`\`<!--${IF_ANCHOR_LABEL}-->\``]),
) )
}
return statement return statement
} }

View File

@ -24,10 +24,10 @@ export function genSelf(
context: CodegenContext, context: CodegenContext,
): CodeFragment[] { ): CodeFragment[] {
const [frag, push] = buildCodeFragment() const [frag, push] = buildCodeFragment()
const { id, template, operation } = dynamic const { id, template, operation, dynamicChildOffset } = dynamic
if (id !== undefined && template !== undefined) { if (id !== undefined && template !== undefined) {
push(NEWLINE, `const n${id} = t${template}()`) push(NEWLINE, `const n${id} = t${template}(${dynamicChildOffset || ''})`)
push(...genDirectivesForElement(id, context)) push(...genDirectivesForElement(id, context))
} }

View File

@ -266,6 +266,7 @@ export interface IRDynamicInfo {
children: IRDynamicInfo[] children: IRDynamicInfo[]
template?: number template?: number
hasDynamicChild?: boolean hasDynamicChild?: boolean
dynamicChildOffset?: number
operation?: OperationNode operation?: OperationNode
needsKey?: boolean needsKey?: boolean
} }

View File

@ -59,7 +59,7 @@ export const transformChildren: NodeTransform = (node, context) => {
function processDynamicChildren(context: TransformContext<ElementNode>) { function processDynamicChildren(context: TransformContext<ElementNode>) {
let prevDynamics: IRDynamicInfo[] = [] let prevDynamics: IRDynamicInfo[] = []
let hasStaticTemplate = false let staticCount = 0
const children = context.dynamic.children const children = context.dynamic.children
for (const [index, child] of children.entries()) { for (const [index, child] of children.entries()) {
@ -69,7 +69,7 @@ function processDynamicChildren(context: TransformContext<ElementNode>) {
if (!(child.flags & DynamicFlag.NON_TEMPLATE)) { if (!(child.flags & DynamicFlag.NON_TEMPLATE)) {
if (prevDynamics.length) { if (prevDynamics.length) {
if (hasStaticTemplate) { if (staticCount) {
// each dynamic child gets its own placeholder node. // each dynamic child gets its own placeholder node.
// this makes it easier to locate the corresponding node during hydration. // this makes it easier to locate the corresponding node during hydration.
for (let i = 0; i < prevDynamics.length; i++) { for (let i = 0; i < prevDynamics.length; i++) {
@ -92,12 +92,13 @@ function processDynamicChildren(context: TransformContext<ElementNode>) {
} }
prevDynamics = [] prevDynamics = []
} }
hasStaticTemplate = true staticCount++
} }
} }
if (prevDynamics.length) { if (prevDynamics.length) {
registerInsertion(prevDynamics, context) registerInsertion(prevDynamics, context, undefined)
context.dynamic.dynamicChildOffset = staticCount
} }
} }

View File

@ -191,6 +191,7 @@ export interface VaporInteropInterface {
transition: TransitionHooks, transition: TransitionHooks,
): void ): void
hydrate(node: Node, fn: () => void): void hydrate(node: Node, fn: () => void): void
hydrateSlot(vnode: VNode, container: any): void
vdomMount: ( vdomMount: (
component: ConcreteComponent, component: ConcreteComponent,

View File

@ -5,6 +5,7 @@ import {
Comment as VComment, Comment as VComment,
type VNode, type VNode,
type VNodeHook, type VNodeHook,
VaporSlot,
createTextVNode, createTextVNode,
createVNode, createVNode,
invokeVNodeHook, invokeVNodeHook,
@ -276,6 +277,12 @@ export function createHydrationFunctions(
) )
} }
break break
case VaporSlot:
getVaporInterface(parentComponent, vnode).hydrateSlot(
vnode,
parentNode(node)!,
)
break
default: default:
if (shapeFlag & ShapeFlags.ELEMENT) { if (shapeFlag & ShapeFlags.ELEMENT) {
if ( if (

View File

@ -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 () => { test('nested consecutive components with anchor insertion', async () => {
const { container, data } = await testHydration( const { container, data } = await testHydration(
` `
@ -1046,6 +1074,27 @@ describe('Vapor Mode hydration', () => {
`</div>`, `</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', () => { describe('if', () => {
@ -1314,6 +1363,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 () => { test('consecutive v-if on component with anchor insertion', async () => {
const data = ref(true) const data = ref(true)
const { container } = await testHydration( const { container } = await testHydration(
@ -2354,6 +2435,31 @@ describe('Vapor Mode hydration', () => {
`</div>`, `</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 () => { describe.todo('transition', async () => {
@ -3912,6 +4018,76 @@ describe('VDOM hydration interop', () => {
expect(container.innerHTML).toMatchInlineSnapshot(`"false"`) 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 () => { test('vapor slot render vdom component', async () => {
const data = ref(true) const data = ref(true)
const { container } = await testHydrationInterop( const { container } = await testHydrationInterop(

View File

@ -30,6 +30,7 @@ import {
isHydrating, isHydrating,
locateHydrationNode, locateHydrationNode,
locateVaporFragmentAnchor, locateVaporFragmentAnchor,
updateNextChildToHydrate,
} from './dom/hydration' } from './dom/hydration'
import { VaporFragment } from './fragment' import { VaporFragment } from './fragment'
import { import {
@ -86,7 +87,7 @@ export const createFor = (
const _insertionParent = insertionParent const _insertionParent = insertionParent
const _insertionAnchor = insertionAnchor const _insertionAnchor = insertionAnchor
if (isHydrating) { if (isHydrating) {
locateHydrationNode(true) locateHydrationNode()
} else { } else {
resetInsertionState() resetInsertionState()
} }
@ -129,6 +130,10 @@ export const createFor = (
if (!isMounted) { if (!isMounted) {
isMounted = true isMounted = true
for (let i = 0; i < newLength; i++) { for (let i = 0; i < newLength; i++) {
// TODO add tests
if (isHydrating && i > 0 && _insertionParent) {
updateNextChildToHydrate(_insertionParent)
}
mount(source, i) mount(source, i)
} }
} else { } else {

View File

@ -66,7 +66,13 @@ import {
} from './componentSlots' } from './componentSlots'
import { hmrReload, hmrRerender } from './hmr' import { hmrReload, hmrRerender } from './hmr'
import { createElement } from './dom/node' import { createElement } from './dom/node'
import { isHydrating, locateHydrationNode } from './dom/hydration' import {
adoptTemplate,
currentHydrationNode,
isHydrating,
locateHydrationNode,
setCurrentHydrationNode,
} from './dom/hydration'
import { isVaporTeleport } from './components/Teleport' import { isVaporTeleport } from './components/Teleport'
import { import {
insertionAnchor, insertionAnchor,
@ -533,7 +539,9 @@ export function createComponentWithFallback(
resetInsertionState() resetInsertionState()
} }
const el = createElement(comp) const el = isHydrating
? (adoptTemplate(currentHydrationNode!, `<${comp}/>`) as HTMLElement)
: createElement(comp)
// mark single root // mark single root
;(el as any).$root = isSingleRoot ;(el as any).$root = isSingleRoot
@ -547,6 +555,7 @@ export function createComponentWithFallback(
} }
if (rawSlots) { if (rawSlots) {
isHydrating && setCurrentHydrationNode(el.firstChild)
if (rawSlots.$) { if (rawSlots.$) {
// TODO dynamic slot fragment // TODO dynamic slot fragment
} else { } else {

View File

@ -6,11 +6,12 @@ import {
setInsertionState, setInsertionState,
} from '../insertionState' } from '../insertionState'
import { import {
__next,
_nthChild,
disableHydrationNodeLookup, disableHydrationNodeLookup,
enableHydrationNodeLookup, enableHydrationNodeLookup,
next,
} from './node' } from './node'
import { isVaporAnchors, isVaporFragmentAnchor } from '@vue/shared' import { isVaporAnchors } from '@vue/shared'
export let isHydrating = false export let isHydrating = false
export let currentHydrationNode: Node | null = null export let currentHydrationNode: Node | null = null
@ -29,9 +30,9 @@ function performHydration<T>(
if (!isOptimized) { if (!isOptimized) {
adoptTemplate = adoptTemplateImpl adoptTemplate = adoptTemplateImpl
locateHydrationNode = locateHydrationNodeImpl locateHydrationNode = locateHydrationNodeImpl
// optimize anchor cache lookup // optimize anchor cache lookup
;(Comment.prototype as any).$fs = undefined ;(Comment.prototype as any).$fe = undefined
;(Node.prototype as any).$dp = undefined
isOptimized = true isOptimized = true
} }
enableHydrationNodeLookup() enableHydrationNodeLookup()
@ -58,12 +59,12 @@ export function hydrateNode(node: Node, fn: () => void): void {
} }
export let adoptTemplate: (node: Node, template: string) => Node | null export let adoptTemplate: (node: Node, template: string) => Node | null
export let locateHydrationNode: (hasFragmentAnchor?: boolean) => void export let locateHydrationNode: (isFragment?: boolean) => void
type Anchor = Comment & { type Anchor = Comment & {
// cached matching fragment start to avoid repeated traversal // cached matching fragment end to avoid repeated traversal
// on nested fragments // on nested fragments
$fs?: Anchor $fe?: Anchor
} }
export const isComment = (node: Node, data: string): node is Anchor => export const isComment = (node: Node, data: string): node is Anchor =>
@ -95,13 +96,23 @@ function adoptTemplateImpl(node: Node, template: string): Node | null {
} }
} }
currentHydrationNode = next(node) currentHydrationNode = __next(node)
return node return node
} }
const hydrationPositionMap = new WeakMap<ParentNode, Node>() const childToHydrateMap = new WeakMap<ParentNode, Node>()
function locateHydrationNodeImpl(hasFragmentAnchor?: boolean) { export function updateNextChildToHydrate(parent: ParentNode): void {
let nextNode = childToHydrateMap.get(parent)
if (nextNode) {
nextNode = __next(nextNode)
if (nextNode) {
childToHydrateMap.set(parent, (currentHydrationNode = nextNode))
}
}
}
function locateHydrationNodeImpl(isFragment?: boolean): void {
let node: Node | null let node: Node | null
// prepend / firstChild // prepend / firstChild
if (insertionAnchor === 0) { if (insertionAnchor === 0) {
@ -112,52 +123,25 @@ function locateHydrationNodeImpl(hasFragmentAnchor?: boolean) {
// SSR Output: `...<span/>Content<span/>...`// `insertionAnchor` is the actual node // SSR Output: `...<span/>Content<span/>...`// `insertionAnchor` is the actual node
node = insertionAnchor node = insertionAnchor
} else { } else {
node = insertionParent node = currentHydrationNode
? hydrationPositionMap.get(insertionParent) || insertionParent.lastChild if (insertionParent && (!node || node.parentNode !== insertionParent)) {
: currentHydrationNode node =
childToHydrateMap.get(insertionParent) ||
// if node is a vapor fragment anchor, find the previous one _nthChild(insertionParent, insertionParent.$dp || 0)
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, ']')) { // locate slot fragment start anchor
// fragment backward search if (isFragment && node && !isComment(node, '[')) {
if (node.$fs) { node = locateVaporFragmentAnchor(node, '[')!
// already cached matching fragment start } else {
node = node.$fs while (node && isNonHydrationNode(node)) {
} else { node = node.nextSibling!
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 (insertionParent && node) { if (insertionParent && node) {
const prev = node.previousSibling const nextNode = node.nextSibling
if (prev) hydrationPositionMap.set(insertionParent, prev) if (nextNode) childToHydrateMap.set(insertionParent, nextNode)
} }
} }
@ -171,24 +155,28 @@ function locateHydrationNodeImpl(hasFragmentAnchor?: boolean) {
} }
export function locateEndAnchor( export function locateEndAnchor(
node: Node | null, node: Anchor,
open = '[', open = '[',
close = ']', close = ']',
): Node | null { ): Node | null {
let match = 0 // already cached matching end
while (node) { if (node.$fe) {
node = node.nextSibling return node.$fe
if (node && node.nodeType === 8) { }
if ((node as Comment).data === open) match++
if ((node as Comment).data === close) { const stack: Anchor[] = [node]
if (match === 0) { while ((node = node.nextSibling as Anchor) && stack.length > 0) {
return node if (node.nodeType === 8) {
} else { if (node.data === open) {
match-- stack.push(node)
} } else if (node.data === close) {
const matchingOpen = stack.pop()!
matchingOpen.$fe = node
if (stack.length === 0) return node
} }
} }
} }
return null return null
} }

View File

@ -6,13 +6,16 @@ let t: HTMLTemplateElement
/*! #__NO_SIDE_EFFECTS__ */ /*! #__NO_SIDE_EFFECTS__ */
export function template(html: string, root?: boolean) { export function template(html: string, root?: boolean) {
let node: Node let node: Node
return (): Node & { $root?: true } => { return (n?: number): Node & { $root?: true } => {
if (isHydrating) { if (isHydrating) {
if (__DEV__ && !currentHydrationNode) { if (__DEV__ && !currentHydrationNode) {
// TODO this should not happen // TODO this should not happen
throw new Error('No current hydration node') 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 // fast path for text nodes
if (html[0] !== '<') { if (html[0] !== '<') {

View File

@ -16,6 +16,7 @@ import {
isHydrating, isHydrating,
locateHydrationNode, locateHydrationNode,
locateVaporFragmentAnchor, locateVaporFragmentAnchor,
setCurrentHydrationNode,
} from './dom/hydration' } from './dom/hydration'
import { import {
applyTransitionHooks, applyTransitionHooks,
@ -60,7 +61,7 @@ export class DynamicFragment extends VaporFragment {
constructor(anchorLabel?: string) { constructor(anchorLabel?: string) {
super([]) super([])
if (isHydrating) { if (isHydrating) {
locateHydrationNode(true) locateHydrationNode(anchorLabel === 'slot')
this.hydrate(anchorLabel!) this.hydrate(anchorLabel!)
} else { } else {
this.anchor = this.anchor =
@ -117,16 +118,23 @@ export class DynamicFragment extends VaporFragment {
parent && insert(this.nodes, parent, this.anchor) parent && insert(this.nodes, parent, this.anchor)
} }
if (isHydrating) {
setCurrentHydrationNode(this.anchor.nextSibling)
}
resetTracking() resetTracking()
} }
hydrate(label: string): void { hydrate(label: string): void {
// for `v-if="false"` the node will be an empty comment, use it as the anchor. // for `v-if="false"` the node will be an empty comment, use it as the anchor.
// otherwise, find next sibling vapor fragment anchor // otherwise, find next sibling vapor fragment anchor
if (isComment(currentHydrationNode!, '')) { if (label === 'if' && isComment(currentHydrationNode!, '')) {
this.anchor = currentHydrationNode this.anchor = currentHydrationNode
} else { } else {
const anchor = locateVaporFragmentAnchor(currentHydrationNode!, label)! let anchor = locateVaporFragmentAnchor(currentHydrationNode!, label)!
if (!anchor && (label === 'slot' || label === 'if')) {
// fallback to fragment end anchor for ssr vdom slot
anchor = locateVaporFragmentAnchor(currentHydrationNode!, ']')!
}
if (anchor) { if (anchor) {
this.anchor = anchor this.anchor = anchor
} else if (__DEV__) { } else if (__DEV__) {

View File

@ -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 export let insertionAnchor: Node | 0 | undefined
/** /**

View File

@ -48,13 +48,15 @@ import {
import { type RawProps, rawPropsProxyHandlers } from './componentProps' import { type RawProps, rawPropsProxyHandlers } from './componentProps'
import type { RawSlots, VaporSlot } from './componentSlots' import type { RawSlots, VaporSlot } from './componentSlots'
import { renderEffect } from './renderEffect' import { renderEffect } from './renderEffect'
import { createTextNode } from './dom/node' import { __next, createTextNode } from './dom/node'
import { optimizePropertyLookup } from './dom/prop' import { optimizePropertyLookup } from './dom/prop'
import { setTransitionHooks as setVaporTransitionHooks } from './components/Transition' import { setTransitionHooks as setVaporTransitionHooks } from './components/Transition'
import { import {
currentHydrationNode, currentHydrationNode,
isHydrating, isHydrating,
locateHydrationNode, locateHydrationNode,
locateVaporFragmentAnchor,
setCurrentHydrationNode,
hydrateNode as vaporHydrateNode, hydrateNode as vaporHydrateNode,
} from './dom/hydration' } from './dom/hydration'
import { DynamicFragment, VaporFragment, isFragment } from './fragment' import { DynamicFragment, VaporFragment, isFragment } from './fragment'
@ -171,6 +173,16 @@ const vaporInteropImpl: Omit<
}, },
hydrate: vaporHydrateNode, 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< const vaporSlotPropsProxyHandler: ProxyHandler<
@ -261,17 +273,7 @@ function createVDOMComponent(
if (transition) setVNodeTransitionHooks(vnode, transition) if (transition) setVNodeTransitionHooks(vnode, transition)
if (isHydrating) { if (isHydrating) {
;( hydrateVNode(vnode, parentInstance as any)
vdomHydrateNode ||
(vdomHydrateNode = ensureHydrationRenderer().hydrateNode!)
)(
currentHydrationNode!,
vnode,
parentInstance as any,
null,
null,
false,
)
} else { } else {
internals.mt( internals.mt(
vnode, vnode,
@ -344,36 +346,26 @@ function renderVDOMSlot(
ensureVaporSlotFallback(children, fallback as any) ensureVaporSlotFallback(children, fallback as any)
isValidSlot = children.length > 0 isValidSlot = children.length > 0
} }
if (isValidSlot) { if (isValidSlot) {
if (isHydrating) { if (isHydrating) {
locateHydrationNode(true) hydrateVNode(vnode!, parentComponent as any)
;( } else {
vdomHydrateNode || if (fallbackNodes) {
(vdomHydrateNode = ensureHydrationRenderer().hydrateNode!) remove(fallbackNodes, parentNode)
)( fallbackNodes = undefined
currentHydrationNode!, }
internals.p(
oldVNode,
vnode!, vnode!,
parentNode,
anchor,
parentComponent as any, parentComponent as any,
null, null,
undefined,
null, null,
false, false,
) )
} else if (fallbackNodes) {
remove(fallbackNodes, parentNode)
fallbackNodes = undefined
} }
internals.p(
oldVNode,
vnode!,
parentNode,
anchor,
parentComponent as any,
null,
undefined,
null,
false,
)
oldVNode = vnode! oldVNode = vnode!
} else { } else {
// for forwarded slot without its own fallback, use the fallback // for forwarded slot without its own fallback, use the fallback
@ -390,6 +382,14 @@ function renderVDOMSlot(
parentNode, parentNode,
anchor, anchor,
) )
} else if (isHydrating) {
// update hydration node to the next sibling of the slot anchor
locateHydrationNode()
const nextNode = locateVaporFragmentAnchor(
currentHydrationNode!,
'slot',
)
if (nextNode) setCurrentHydrationNode(__next(nextNode))
} }
oldVNode = null oldVNode = null
} }
@ -397,13 +397,15 @@ function renderVDOMSlot(
isMounted = true isMounted = true
} else { } else {
// move // move
internals.m( if (oldVNode && !isHydrating) {
oldVNode!, internals.m(
parentNode, oldVNode,
anchor, parentNode,
MoveType.REORDER, anchor,
parentComponent as any, MoveType.REORDER,
) parentComponent as any,
)
}
} }
frag.remove = parentNode => { frag.remove = parentNode => {
@ -451,7 +453,12 @@ const createFallback =
const frag = new VaporFragment([]) const frag = new VaporFragment([])
frag.insert = (parentNode, anchor) => { frag.insert = (parentNode, anchor) => {
fallbackNodes.forEach(vnode => { 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 => { frag.remove = parentNode => {
@ -465,3 +472,22 @@ const createFallback =
// vapor slot // vapor slot
return fallbackNodes as Block 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
locateHydrationNode()
if (!vdomHydrateNode) vdomHydrateNode = ensureHydrationRenderer().hydrateNode!
const nextNode = vdomHydrateNode(
currentHydrationNode!,
vnode,
parentComponent,
null,
null,
false,
)
setCurrentHydrationNode(nextNode)
}

View File

@ -28,7 +28,7 @@ describe('ssr: attr fallthrough', () => {
`<div class="foo bar"></div><!--if-->`, `<div class="foo bar"></div><!--if-->`,
) )
expect(await renderToString(createApp(Parent, { ok: false }))).toBe( expect(await renderToString(createApp(Parent, { ok: false }))).toBe(
`<span class="bar"></span>`, `<span class="bar"></span><!--if-->`,
) )
}) })