mirror of https://github.com/vuejs/core.git
wip: hydration
This commit is contained in:
parent
bba71becba
commit
63ec52aaa9
|
@ -77,6 +77,7 @@ describe('ssr: inject <style vars>', () => {
|
||||||
}></div><div\${
|
}></div><div\${
|
||||||
_ssrRenderAttrs(_cssVars)
|
_ssrRenderAttrs(_cssVars)
|
||||||
}></div><!--]-->\`)
|
}></div><!--]-->\`)
|
||||||
|
_push(\`<!--if-->\`)
|
||||||
}
|
}
|
||||||
}"
|
}"
|
||||||
`)
|
`)
|
||||||
|
|
|
@ -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-->\`)
|
||||||
}
|
}
|
||||||
}"
|
}"
|
||||||
`)
|
`)
|
||||||
|
|
|
@ -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
|
||||||
}
|
}
|
||||||
|
|
|
@ -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))
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -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
|
||||||
}
|
}
|
||||||
|
|
|
@ -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
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -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,
|
||||||
|
|
|
@ -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 (
|
||||||
|
|
|
@ -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(
|
||||||
|
|
|
@ -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 {
|
||||||
|
|
|
@ -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 {
|
||||||
|
|
|
@ -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
|
|
||||||
node = node.$fs
|
|
||||||
} else {
|
} else {
|
||||||
let cur: Node | null = node
|
while (node && isNonHydrationNode(node)) {
|
||||||
let curFragEnd = node
|
node = node.nextSibling!
|
||||||
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
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -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] !== '<') {
|
||||||
|
|
|
@ -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__) {
|
||||||
|
|
|
@ -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
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|
|
@ -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,22 +346,11 @@ 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!)
|
|
||||||
)(
|
|
||||||
currentHydrationNode!,
|
|
||||||
vnode!,
|
|
||||||
parentComponent as any,
|
|
||||||
null,
|
|
||||||
null,
|
|
||||||
false,
|
|
||||||
)
|
|
||||||
} else if (fallbackNodes) {
|
|
||||||
remove(fallbackNodes, parentNode)
|
remove(fallbackNodes, parentNode)
|
||||||
fallbackNodes = undefined
|
fallbackNodes = undefined
|
||||||
}
|
}
|
||||||
|
@ -374,6 +365,7 @@ function renderVDOMSlot(
|
||||||
null,
|
null,
|
||||||
false,
|
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,14 +397,16 @@ function renderVDOMSlot(
|
||||||
isMounted = true
|
isMounted = true
|
||||||
} else {
|
} else {
|
||||||
// move
|
// move
|
||||||
|
if (oldVNode && !isHydrating) {
|
||||||
internals.m(
|
internals.m(
|
||||||
oldVNode!,
|
oldVNode,
|
||||||
parentNode,
|
parentNode,
|
||||||
anchor,
|
anchor,
|
||||||
MoveType.REORDER,
|
MoveType.REORDER,
|
||||||
parentComponent as any,
|
parentComponent as any,
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
frag.remove = parentNode => {
|
frag.remove = parentNode => {
|
||||||
if (fallbackNodes) {
|
if (fallbackNodes) {
|
||||||
|
@ -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 => {
|
||||||
|
// hydrate fallback
|
||||||
|
if (isHydrating) {
|
||||||
|
hydrateVNode(vnode, parentComponent as any)
|
||||||
|
} else {
|
||||||
internals.p(null, vnode, parentNode, anchor, parentComponent)
|
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)
|
||||||
|
}
|
||||||
|
|
|
@ -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-->`,
|
||||||
)
|
)
|
||||||
})
|
})
|
||||||
|
|
||||||
|
|
Loading…
Reference in New Issue