wip: hydrate vapor teleport (#13864)

This commit is contained in:
edison 2025-09-28 16:23:43 +08:00 committed by GitHub
parent 2802c0f4dc
commit 12e41691c3
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
29 changed files with 1178 additions and 454 deletions

View File

@ -157,9 +157,9 @@ export function render(_ctx, $props, $emit, $attrs, $slots) {
const _component_Comp = _resolveComponent("Comp")
const n0 = t0()
const n3 = t1()
const n2 = _child(n3, 1)
_setInsertionState(n3, 0)
const n1 = _createComponentWithFallback(_component_Comp)
const n2 = _child(n3)
_renderEffect(() => {
_setProp(n3, "id", _ctx.foo)
_setText(n2, _toDisplayString(_ctx.bar))
@ -212,6 +212,30 @@ export function render(_ctx) {
}"
`;
exports[`compile > execution order > setInsertionState > next, child and nthChild should be above the setInsertionState 1`] = `
"import { resolveComponent as _resolveComponent, child as _child, next as _next, setInsertionState as _setInsertionState, createComponentWithFallback as _createComponentWithFallback, nthChild as _nthChild, createIf as _createIf, setProp as _setProp, renderEffect as _renderEffect, template as _template } from 'vue';
const t0 = _template("<div></div>")
const t1 = _template("<div><div></div><!><div></div><!><div><button></button></div></div>", true)
export function render(_ctx) {
const _component_Comp = _resolveComponent("Comp")
const n6 = t1()
const n5 = _next(_child(n6), 1)
const n7 = _nthChild(n6, 3, 3)
const p0 = _next(n7, 4)
const n4 = _child(p0, 0)
_setInsertionState(n6, n5)
const n0 = _createComponentWithFallback(_component_Comp)
_setInsertionState(n6, n7)
const n1 = _createIf(() => (true), () => {
const n3 = t0()
return n3
})
_renderEffect(() => _setProp(n4, "disabled", _ctx.foo))
return n6
}"
`;
exports[`compile > execution order > with insertionState 1`] = `
"import { resolveComponent as _resolveComponent, child as _child, setInsertionState as _setInsertionState, createSlot as _createSlot, createComponentWithFallback as _createComponentWithFallback, template as _template } from 'vue';
const t0 = _template("<div><div></div></div>", true)
@ -219,7 +243,7 @@ const t0 = _template("<div><div></div></div>", true)
export function render(_ctx) {
const _component_Comp = _resolveComponent("Comp")
const n3 = t0()
const n1 = _child(n3)
const n1 = _child(n3, 0)
_setInsertionState(n1, null)
const n0 = _createSlot("default", null)
_setInsertionState(n3, 1)
@ -234,9 +258,9 @@ const t0 = _template("<div><span> </span> <br> </div>", true)
export function render(_ctx) {
const n3 = t0()
const n0 = _child(n3)
const n1 = _next(n0)
const n2 = _nthChild(n3, 3)
const n0 = _child(n3, 0)
const n1 = _next(n0, 1)
const n2 = _nthChild(n3, 3, 3)
const x0 = _txt(n0)
_setText(x0, _toDisplayString(_ctx.foo))
_renderEffect(() => {

View File

@ -231,6 +231,23 @@ describe('compile', () => {
)
})
describe('setInsertionState', () => {
test('next, child and nthChild should be above the setInsertionState', () => {
const code = compile(`
<div>
<div />
<Comp />
<div />
<div v-if="true" />
<div>
<button :disabled="foo" />
</div>
</div>
`)
expect(code).toMatchSnapshot()
})
})
test('with v-once', () => {
const code = compile(
`<div>

View File

@ -47,7 +47,7 @@ const t0 = _template("<div> </div>", true)
export function render(_ctx) {
const n1 = t0()
const n0 = _child(n1)
const n0 = _child(n1, 0)
const x1 = _txt(n1)
_renderEffect(() => {
const _foo = _ctx.foo
@ -86,7 +86,7 @@ const t0 = _template("<div> </div>", true)
export function render(_ctx) {
const n1 = t0()
const n0 = _child(n1)
const n0 = _child(n1, 0)
const x1 = _txt(n1)
_renderEffect(() => {
const _String = String

View File

@ -7,7 +7,7 @@ const t1 = _template("<div><div></div><!><div></div></div>", true)
export function render(_ctx) {
const n4 = t1()
const n3 = _next(_child(n4))
const n3 = _next(_child(n4), 1)
_setInsertionState(n4, n3)
const n0 = _createIf(() => (1), () => {
const n2 = t0()
@ -23,9 +23,9 @@ const t0 = _template("<div><p> </p> <p> </p></div>", true)
export function render(_ctx) {
const n3 = t0()
const n0 = _child(n3)
const n1 = _next(n0)
const n2 = _next(n1)
const n0 = _child(n3, 0)
const n1 = _next(n0, 1)
const n2 = _next(n1, 2)
const x0 = _txt(n0)
const x2 = _txt(n2)
_renderEffect(() => {
@ -43,7 +43,7 @@ const t0 = _template("<div><div>x</div><div>x</div><div> </div></div>", true)
export function render(_ctx) {
const n1 = t0()
const n0 = _nthChild(n1, 2)
const n0 = _nthChild(n1, 2, 2)
const x0 = _txt(n0)
_renderEffect(() => _setText(x0, _toDisplayString(_ctx.msg)))
return n1
@ -56,12 +56,12 @@ const t0 = _template("<div><div>x</div><div><span> </span></div><div><span> </sp
export function render(_ctx) {
const n3 = t0()
const p0 = _next(_child(n3))
const n0 = _child(p0)
const p1 = _next(p0)
const n1 = _child(p1)
const p2 = _next(p1)
const n2 = _child(p2)
const p0 = _next(_child(n3), 1)
const n0 = _child(p0, 0)
const p1 = _next(p0, 2)
const n1 = _child(p1, 0)
const p2 = _next(p1, 3)
const n2 = _child(p2, 0)
const x0 = _txt(n0)
const x1 = _txt(n1)
const x2 = _txt(n2)

View File

@ -243,7 +243,7 @@ export function render(_ctx) {
const _component_Comp = _resolveComponent("Comp")
const n0 = _createFor(() => (_ctx.list), (_for_item0) => {
const n3 = _createComponentWithFallback(_component_Comp)
const n2 = _child(n3)
const n2 = _child(n3, 0)
_renderEffect(() => _setText(n2, _toDisplayString(_for_item0.value)))
return [n2, n3]
}, undefined, 2)
@ -259,7 +259,7 @@ export function render(_ctx) {
const _component_Comp = _resolveComponent("Comp")
const n0 = _createFor(() => (_ctx.list), (_for_item0) => {
const n3 = _createComponentWithFallback(_component_Comp)
const n2 = _child(n3)
const n2 = _child(n3, 0)
_renderEffect(() => _setText(n2, _toDisplayString(_for_item0.value)))
return [n2, n3]
}, undefined, 2)

View File

@ -17,8 +17,8 @@ const t0 = _template("<div> <span></span></div>", true)
export function render(_ctx, $props, $emit, $attrs, $slots) {
const n2 = t0()
const n0 = _child(n2)
const n1 = _next(n0)
const n0 = _child(n2, 0)
const n1 = _next(n0, 1)
_setText(n0, _toDisplayString(_ctx.msg) + " ")
_setClass(n1, _ctx.clz)
return n2
@ -54,7 +54,7 @@ const t0 = _template("<div><div></div></div>", true)
export function render(_ctx) {
const n1 = t0()
const n0 = _child(n1)
const n0 = _child(n1, 0)
_setProp(n0, "id", _ctx.foo)
return n1
}"

View File

@ -56,7 +56,7 @@ describe('compiler: children transform', () => {
<div>{{ msg }}</div>
</div>`,
)
expect(code).contains(`const n0 = _nthChild(n1, 2)`)
expect(code).contains(`const n0 = _nthChild(n1, 2, 2)`)
expect(code).toMatchSnapshot()
})
@ -69,7 +69,7 @@ describe('compiler: children transform', () => {
</div>`,
)
// ensure the insertion anchor is generated before the insertion statement
expect(code).toMatch(`const n3 = _next(_child(n4))`)
expect(code).toMatch(`const n3 = _next(_child(n4), 1)`)
expect(code).toMatch(`_setInsertionState(n4, n3)`)
expect(code).toMatchSnapshot()
})

View File

@ -71,7 +71,7 @@ export function genBlockContent(
}
for (const child of dynamic.children) {
if (!child.hasDynamicChild) {
push(...genChildren(child, context, `n${child.id!}`))
push(...genChildren(child, context, push, `n${child.id!}`))
}
}

View File

@ -1,5 +1,9 @@
import type { CodegenContext } from '../generate'
import { DynamicFlag, type IRDynamicInfo } from '../ir'
import {
DynamicFlag,
type IRDynamicInfo,
type InsertionStateTypes,
} from '../ir'
import { genDirectivesForElement } from './directive'
import { genOperationWithInsertionState } from './operation'
import { type CodeFragment, NEWLINE, buildCodeFragment, genCall } from './utils'
@ -36,7 +40,7 @@ export function genSelf(
}
if (hasDynamicChild) {
push(...genChildren(dynamic, context, `n${id}`))
push(...genChildren(dynamic, context, push, `n${id}`))
}
return frag
@ -45,6 +49,7 @@ export function genSelf(
export function genChildren(
dynamic: IRDynamicInfo,
context: CodegenContext,
pushBlock: (...items: CodeFragment[]) => number,
from: string = `n${dynamic.id}`,
): CodeFragment[] {
const { helper } = context
@ -53,10 +58,20 @@ export function genChildren(
let offset = 0
let prev: [variable: string, elementIndex: number] | undefined
let ifBranchCount = 0
let prependCount = 0
for (const [index, child] of children.entries()) {
if (
child.operation &&
(child.operation as InsertionStateTypes).anchor === -1
) {
prependCount++
}
if (child.flags & DynamicFlag.NON_TEMPLATE) {
offset--
} else if (child.ifBranch) {
ifBranchCount++
}
const id =
@ -72,29 +87,41 @@ export function genChildren(
}
const elementIndex = index + offset
const logicalIndex = elementIndex - ifBranchCount + prependCount
// p for "placeholder" variables that are meant for possible reuse by
// other access paths
const variable = id === undefined ? `p${context.block.tempId++}` : `n${id}`
push(NEWLINE, `const ${variable} = `)
pushBlock(NEWLINE, `const ${variable} = `)
if (prev) {
if (elementIndex - prev[1] === 1) {
push(...genCall(helper('next'), prev[0]))
pushBlock(...genCall(helper('next'), prev[0], String(logicalIndex)))
} else {
push(...genCall(helper('nthChild'), from, String(elementIndex)))
pushBlock(
...genCall(
helper('nthChild'),
from,
String(elementIndex),
String(logicalIndex),
),
)
}
} else {
if (elementIndex === 0) {
push(...genCall(helper('child'), from))
pushBlock(...genCall(helper('child'), from, String(logicalIndex)))
} else {
// check if there's a node that we can reuse from
let init = genCall(helper('child'), from)
if (elementIndex === 1) {
init = genCall(helper('next'), init)
init = genCall(helper('next'), init, String(logicalIndex))
} else if (elementIndex > 1) {
init = genCall(helper('nthChild'), from, String(elementIndex))
init = genCall(
helper('nthChild'),
from,
String(elementIndex),
String(logicalIndex),
)
}
push(...init)
pushBlock(...init)
}
}
@ -107,7 +134,7 @@ export function genChildren(
}
prev = [variable, elementIndex]
push(...genChildren(child, context, variable))
push(...genChildren(child, context, pushBlock, variable))
}
return frag

View File

@ -272,6 +272,7 @@ export interface IRDynamicInfo {
hasDynamicChild?: boolean
operation?: OperationNode
needsKey?: boolean
ifBranch?: boolean
}
export interface IREffect {

View File

@ -16,6 +16,7 @@ import {
isConstantExpression,
isStaticExpression,
} from '../utils'
import { escapeHtml } from '@vue/shared'
type TextLike = TextNode | InterpolationNode
const seen = new WeakMap<
@ -82,7 +83,7 @@ export const transformText: NodeTransform = (node, context) => {
} else if (node.type === NodeTypes.INTERPOLATION) {
processInterpolation(context as TransformContext<InterpolationNode>)
} else if (node.type === NodeTypes.TEXT) {
context.template += node.content
context.template += escapeHtml(node.content)
}
}
@ -143,7 +144,7 @@ function processTextContainer(
const literals = values.map(getLiteralExpressionValue)
if (literals.every(l => l != null)) {
context.childrenTemplate = literals.map(l => String(l))
context.childrenTemplate = literals.map(l => escapeHtml(String(l)))
} else {
context.childrenTemplate = [' ']
context.registerOperation({

View File

@ -59,6 +59,7 @@ export function processIf(
} else {
// check the adjacent v-if
const siblingIf = getSiblingIf(context, true)
context.dynamic.ifBranch = true
const siblings = context.parent && context.parent.dynamic.children
let lastIfNode

View File

@ -394,7 +394,7 @@ function moveTeleport(
}
}
interface TeleportTargetElement extends Element {
export interface TeleportTargetElement extends Element {
// last teleport target
_lpa?: Node | null
}

View File

@ -659,6 +659,17 @@ export function createHydrationFunctions(
)
}
}
// the server output does not contain blank text nodes. It appears here that
// it is a dynamically inserted anchor, and needs to be skipped.
// e.g. vaporInteropImpl.mount() > selfAnchor
if (
node &&
node.nodeType === DOMNodeTypes.TEXT &&
!(node as Text).data.trim()
) {
node = nextSibling(node)
}
return node
}

View File

@ -584,6 +584,13 @@ export {
isTeleportDisabled,
isTeleportDeferred,
} from './components/Teleport'
/**
* @internal
*/
export type { TeleportTargetElement } from './components/Teleport'
/**
* @internal
*/
export {
createAsyncComponentContext,
useAsyncComponentState,

View File

@ -5,6 +5,8 @@ import * as runtimeVapor from '../src'
import * as runtimeDom from '@vue/runtime-dom'
import * as VueServerRenderer from '@vue/server-renderer'
import { isString } from '@vue/shared'
import type { VaporComponentInstance } from '../src/component'
import type { TeleportFragment } from '../src/components/Teleport'
const formatHtml = (raw: string) => {
return raw
@ -77,22 +79,34 @@ async function testWithVDOMApp(
})
}
function compileVaporComponent(
code: string,
data: runtimeDom.Ref<any> = ref({}),
components?: Record<string, any>,
ssr = false,
) {
return compile(`<template>${code}</template>`, data, components, {
vapor: true,
ssr,
})
}
async function mountWithHydration(
html: string,
code: string,
data: runtimeDom.Ref<any>,
data: runtimeDom.Ref<any> = ref({}),
components?: Record<string, any>,
) {
const container = document.createElement('div')
container.innerHTML = html
document.body.appendChild(container)
const clientComp = compile(`<template>${code}</template>`, data, undefined, {
vapor: true,
ssr: false,
})
const clientComp = compileVaporComponent(code, data, components)
const app = createVaporSSRApp(clientComp)
app.mount(container)
return {
block: (app._instance! as VaporComponentInstance).block,
container,
}
}
@ -298,7 +312,7 @@ describe('Vapor Mode hydration', () => {
<template><!----></template>
`)
expect(formatHtml(container.innerHTML)).toMatchInlineSnapshot(`"<!---->"`)
expect(`Hydration children mismatch in <div>`).not.toHaveBeenWarned()
expect(`mismatch in <div>`).not.toHaveBeenWarned()
})
test('root with mixed element and text', async () => {
@ -331,7 +345,7 @@ describe('Vapor Mode hydration', () => {
expect(formatHtml(container.innerHTML)).toMatchInlineSnapshot(
`"<div></div>"`,
)
expect(`Hydration children mismatch in <div>`).not.toHaveBeenWarned()
expect(`mismatch in <div>`).not.toHaveBeenWarned()
})
test('element with binding and text children', async () => {
@ -1588,7 +1602,8 @@ describe('Vapor Mode hydration', () => {
expect(formatHtml(container.innerHTML)).toMatchInlineSnapshot(
`
"
<!--[--><span>a</span><span>b</span><span>c</span><!--for-->"
<!--[--><span>a</span><span>b</span><span>c</span><!--]-->
"
`,
)
@ -1597,7 +1612,35 @@ describe('Vapor Mode hydration', () => {
expect(formatHtml(container.innerHTML)).toMatchInlineSnapshot(
`
"
<!--[--><span>a</span><span>b</span><span>c</span><span>d</span><!--for-->"
<!--[--><span>a</span><span>b</span><span>c</span><span>d</span><!--]-->
"
`,
)
})
test('empty v-for', async () => {
const { container, data } = await testHydration(
`<template>
<span v-for="item in data" :key="item">{{ item }}</span>
</template>`,
undefined,
ref([]),
)
expect(formatHtml(container.innerHTML)).toMatchInlineSnapshot(
`
"
<!--[--><!--]-->
"
`,
)
data.value.push('a')
await nextTick()
expect(formatHtml(container.innerHTML)).toMatchInlineSnapshot(
`
"
<!--[--><span>a</span><!--]-->
"
`,
)
})
@ -1619,7 +1662,8 @@ describe('Vapor Mode hydration', () => {
`
"
<!--[--><div>
<!--[--><span>a</span><span>b</span><span>c</span><!--for--></div><div>3</div><!--]-->
<!--[--><span>a</span><span>b</span><span>c</span><!--]-->
</div><div>3</div><!--]-->
"
`,
)
@ -1630,7 +1674,8 @@ describe('Vapor Mode hydration', () => {
`
"
<!--[--><div>
<!--[--><span>a</span><span>b</span><span>c</span><span>d</span><!--for--></div><div>4</div><!--]-->
<!--[--><span>a</span><span>b</span><span>c</span><span>d</span><!--]-->
</div><div>4</div><!--]-->
"
`,
)
@ -1651,7 +1696,8 @@ describe('Vapor Mode hydration', () => {
expect(formatHtml(container.innerHTML)).toMatchInlineSnapshot(
`
"<div><span></span>
<!--[--><span>a</span><span>b</span><span>c</span><!--for--><span></span></div>"
<!--[--><span>a</span><span>b</span><span>c</span><!--]-->
<span></span></div>"
`,
)
@ -1660,7 +1706,8 @@ describe('Vapor Mode hydration', () => {
expect(formatHtml(container.innerHTML)).toMatchInlineSnapshot(
`
"<div><span></span>
<!--[--><span>a</span><span>b</span><span>c</span><span>d</span><!--for--><span></span></div>"
<!--[--><span>a</span><span>b</span><span>c</span><span>d</span><!--]-->
<span></span></div>"
`,
)
@ -1669,7 +1716,8 @@ describe('Vapor Mode hydration', () => {
expect(formatHtml(container.innerHTML)).toMatchInlineSnapshot(
`
"<div><span></span>
<!--[--><span>b</span><span>c</span><span>d</span><!--for--><span></span></div>"
<!--[--><span>b</span><span>c</span><span>d</span><!--]-->
<span></span></div>"
`,
)
})
@ -1690,8 +1738,9 @@ describe('Vapor Mode hydration', () => {
expect(formatHtml(container.innerHTML)).toMatchInlineSnapshot(
`
"<div><span></span>
<!--[--><span>a</span><span>b</span><span>c</span><!--for-->
<!--[--><span>a</span><span>b</span><span>c</span><!--for--><span></span></div>"
<!--[--><span>a</span><span>b</span><span>c</span><!--]-->
<!--[--><span>a</span><span>b</span><span>c</span><!--]-->
<span></span></div>"
`,
)
@ -1700,8 +1749,9 @@ describe('Vapor Mode hydration', () => {
expect(formatHtml(container.innerHTML)).toMatchInlineSnapshot(
`
"<div><span></span>
<!--[--><span>a</span><span>b</span><span>c</span><span>d</span><!--for-->
<!--[--><span>a</span><span>b</span><span>c</span><span>d</span><!--for--><span></span></div>"
<!--[--><span>a</span><span>b</span><span>c</span><span>d</span><!--]-->
<!--[--><span>a</span><span>b</span><span>c</span><span>d</span><!--]-->
<span></span></div>"
`,
)
@ -1710,8 +1760,9 @@ describe('Vapor Mode hydration', () => {
expect(formatHtml(container.innerHTML)).toMatchInlineSnapshot(
`
"<div><span></span>
<!--[--><span>c</span><span>d</span><!--for-->
<!--[--><span>c</span><span>d</span><!--for--><span></span></div>"
<!--[--><span>c</span><span>d</span><!--]-->
<!--[--><span>c</span><span>d</span><!--]-->
<span></span></div>"
`,
)
})
@ -1732,7 +1783,8 @@ describe('Vapor Mode hydration', () => {
expect(formatHtml(container.innerHTML)).toMatchInlineSnapshot(
`
"<div>
<!--[--><div>comp</div><div>comp</div><div>comp</div><!--for--></div>"
<!--[--><div>comp</div><div>comp</div><div>comp</div><!--]-->
</div>"
`,
)
@ -1741,7 +1793,8 @@ describe('Vapor Mode hydration', () => {
expect(formatHtml(container.innerHTML)).toMatchInlineSnapshot(
`
"<div>
<!--[--><div>comp</div><div>comp</div><div>comp</div><div>comp</div><!--for--></div>"
<!--[--><div>comp</div><div>comp</div><div>comp</div><div>comp</div><!--]-->
</div>"
`,
)
})
@ -1767,7 +1820,8 @@ describe('Vapor Mode hydration', () => {
<!--[--><span>a</span><!--]-->
<!--[--><span>b</span><!--]-->
<!--[--><span>c</span><!--]-->
<!--for--></div>"
<!--]-->
</div>"
`,
)
@ -1780,7 +1834,8 @@ describe('Vapor Mode hydration', () => {
<!--[--><span>a</span><!--]-->
<!--[--><span>b</span><!--]-->
<!--[--><span>c</span><!--]-->
<span>d</span><!--slot--><!--for--></div>"
<span>d</span><!--slot--><!--]-->
</div>"
`,
)
})
@ -1803,7 +1858,8 @@ describe('Vapor Mode hydration', () => {
<!--[-->
<!--[--><div>foo</div>-bar-<!--]-->
<!--[--><div>foo</div>-bar-<!--]-->
<!--[--><div>foo</div>-bar-<!--for--><!--]-->
<!--[--><div>foo</div>-bar-<!--]-->
<!--]-->
</div>"
`,
)
@ -1816,7 +1872,8 @@ describe('Vapor Mode hydration', () => {
<!--[-->
<!--[--><div>foo</div>-bar-<!--]-->
<!--[--><div>foo</div>-bar-<!--]-->
<!--[--><div>foo</div>-bar-<div>foo</div>-bar-<!--for--><!--]-->
<!--[--><div>foo</div>-bar-<div>foo</div>-bar-<!--]-->
<!--]-->
</div>"
`,
)
@ -1845,7 +1902,8 @@ describe('Vapor Mode hydration', () => {
expect(formatHtml(container.innerHTML)).toMatchInlineSnapshot(
`
"<div>
<!--[--><div><div><div>foo</div><!--if--></div><span>non-hydration node</span></div><div><div><div>foo</div><!--if--></div><span>non-hydration node</span></div><!--for--></div>"
<!--[--><div><div><div>foo</div><!--if--></div><span>non-hydration node</span></div><div><div><div>foo</div><!--if--></div><span>non-hydration node</span></div><!--]-->
</div>"
`,
)
@ -1854,7 +1912,8 @@ describe('Vapor Mode hydration', () => {
expect(formatHtml(container.innerHTML)).toMatchInlineSnapshot(
`
"<div>
<!--[--><div><div><div>bar</div><!--if--></div><span>non-hydration node</span></div><div><div><div>bar</div><!--if--></div><span>non-hydration node</span></div><!--for--></div>"
<!--[--><div><div><div>bar</div><!--if--></div><span>non-hydration node</span></div><div><div><div>bar</div><!--if--></div><span>non-hydration node</span></div><!--]-->
</div>"
`,
)
@ -1862,14 +1921,16 @@ describe('Vapor Mode hydration', () => {
await nextTick()
expect(formatHtml(container.innerHTML)).toMatchInlineSnapshot(`
"<div>
<!--[--><div><div><!--if--></div><span>non-hydration node</span></div><div><div><!--if--></div><span>non-hydration node</span></div><!--for--></div>"
<!--[--><div><div><!--if--></div><span>non-hydration node</span></div><div><div><!--if--></div><span>non-hydration node</span></div><!--]-->
</div>"
`)
data.value.show = true
await nextTick()
expect(formatHtml(container.innerHTML)).toMatchInlineSnapshot(`
"<div>
<!--[--><div><div><div>bar</div><!--if--></div><span>non-hydration node</span></div><div><div><div>bar</div><!--if--></div><span>non-hydration node</span></div><!--for--></div>"
<!--[--><div><div><div>bar</div><!--if--></div><span>non-hydration node</span></div><div><div><div>bar</div><!--if--></div><span>non-hydration node</span></div><!--]-->
</div>"
`)
})
})
@ -2005,7 +2066,8 @@ describe('Vapor Mode hydration', () => {
`
"
<!--[-->
<!--[--><span>a</span><span>b</span><span>c</span><!--for--><!--]-->
<!--[--><span>a</span><span>b</span><span>c</span><!--]-->
<!--]-->
"
`,
)
@ -2437,9 +2499,10 @@ describe('Vapor Mode hydration', () => {
`
"
<!--[-->
<!--[--><div>a</div><div>b</div><div>c</div><!--for-->
<!--[--><div>a</div><div>b</div><div>c</div><!--]-->
<!--[--><span>foo</span><!--]-->
<!--[--><div>a</div><div>b</div><div>c</div><!--for--><!--]-->
<!--[--><div>a</div><div>b</div><div>c</div><!--]-->
<!--]-->
"
`,
)
@ -2450,9 +2513,10 @@ describe('Vapor Mode hydration', () => {
`
"
<!--[-->
<!--[--><div>a</div><div>b</div><div>c</div><div>d</div><!--for-->
<!--[--><div>a</div><div>b</div><div>c</div><div>d</div><!--]-->
<!--[--><span>foo</span><!--]-->
<!--[--><div>a</div><div>b</div><div>c</div><div>d</div><!--for--><!--]-->
<!--[--><div>a</div><div>b</div><div>c</div><div>d</div><!--]-->
<!--]-->
"
`,
)
@ -2920,7 +2984,587 @@ describe('Vapor Mode hydration', () => {
test.todo('force hydrate custom element with dynamic props', () => {})
})
describe.todo('Teleport')
describe('Teleport', () => {
test('basic', async () => {
const data = ref({
msg: ref('foo'),
disabled: ref(false),
fn: vi.fn(),
})
const teleportContainer = document.createElement('div')
teleportContainer.id = 'teleport'
teleportContainer.innerHTML =
`<!--teleport start anchor-->` +
`<span>foo</span>` +
`<span class="foo"></span>` +
`<!--teleport anchor-->`
document.body.appendChild(teleportContainer)
const { block, container } = await mountWithHydration(
'<!--teleport start--><!--teleport end-->',
`<teleport to="#teleport" :disabled="data.disabled">
<span>{{data.msg}}</span>
<span :class="data.msg" @click="data.fn"></span>
</teleport>`,
data,
)
const teleport = block as TeleportFragment
expect(teleport.anchor).toBe(container.lastChild)
expect(teleport.target).toBe(teleportContainer)
expect(teleport.targetStart).toBe(teleportContainer.childNodes[0])
expect((teleport.nodes as Node[])[0]).toBe(
teleportContainer.childNodes[1],
)
expect((teleport.nodes as Node[])[1]).toBe(
teleportContainer.childNodes[2],
)
expect(teleport.targetAnchor).toBe(teleportContainer.childNodes[3])
expect(container.innerHTML).toMatchInlineSnapshot(
`"<!--teleport start--><!--teleport end-->"`,
)
// event handler
triggerEvent('click', teleportContainer.querySelector('.foo')!)
expect(data.value.fn).toHaveBeenCalled()
data.value.msg = 'bar'
await nextTick()
expect(formatHtml(teleportContainer.innerHTML)).toBe(
`<!--teleport start anchor-->` +
`<span>bar</span>` +
`<span class="bar"></span>` +
`<!--teleport anchor-->`,
)
data.value.disabled = true
await nextTick()
expect(container.innerHTML).toBe(
`<!--teleport start-->` +
`<span>bar</span>` +
`<span class="bar"></span>` +
`<!--teleport end-->`,
)
expect(formatHtml(teleportContainer.innerHTML)).toMatchInlineSnapshot(
`"<!--teleport start anchor--><!--teleport anchor-->"`,
)
data.value.msg = 'baz'
await nextTick()
expect(container.innerHTML).toBe(
`<!--teleport start-->` +
`<span>baz</span>` +
`<span class="baz"></span>` +
`<!--teleport end-->`,
)
data.value.disabled = false
await nextTick()
expect(container.innerHTML).toMatchInlineSnapshot(
`"<!--teleport start--><!--teleport end-->"`,
)
expect(formatHtml(teleportContainer.innerHTML)).toBe(
`<!--teleport start anchor-->` +
`<span>baz</span>` +
`<span class="baz"></span>` +
`<!--teleport anchor-->`,
)
})
test('multiple + integration', async () => {
const data = ref({
msg: ref('foo'),
fn1: vi.fn(),
fn2: vi.fn(),
})
const code = `
<teleport to="#teleport2">
<span>{{data.msg}}</span>
<span :class="data.msg" @click="data.fn1"></span>
</teleport>
<teleport to="#teleport2">
<span>{{data.msg}}2</span>
<span :class="data.msg + 2" @click="data.fn2"></span>
</teleport>`
const SSRComp = compileVaporComponent(code, data, undefined, true)
const teleportContainer = document.createElement('div')
teleportContainer.id = 'teleport2'
const ctx = {} as any
const mainHtml = await VueServerRenderer.renderToString(
runtimeDom.createSSRApp(SSRComp),
ctx,
)
expect(mainHtml).toBe(
`<!--[-->` +
`<!--teleport start--><!--teleport end-->` +
`<!--teleport start--><!--teleport end-->` +
`<!--]-->`,
)
const teleportHtml = ctx.teleports!['#teleport2']
expect(teleportHtml).toBe(
`<!--teleport start anchor-->` +
`<span>foo</span><span class="foo"></span>` +
`<!--teleport anchor-->` +
`<!--teleport start anchor-->` +
`<span>foo2</span><span class="foo2"></span>` +
`<!--teleport anchor-->`,
)
teleportContainer.innerHTML = teleportHtml
document.body.appendChild(teleportContainer)
const { block, container } = await mountWithHydration(
mainHtml,
code,
data,
)
const teleports = block as any as TeleportFragment[]
const teleport1 = teleports[0]
const teleport2 = teleports[1]
expect(teleport1.anchor).toBe(container.childNodes[2])
expect(teleport2.anchor).toBe(container.childNodes[4])
expect(teleport1.target).toBe(teleportContainer)
expect(teleport1.targetStart).toBe(teleportContainer.childNodes[0])
expect((teleport1.nodes as Node[])[0]).toBe(
teleportContainer.childNodes[1],
)
expect(teleport1.targetAnchor).toBe(teleportContainer.childNodes[3])
expect(teleport2.target).toBe(teleportContainer)
expect(teleport2.targetStart).toBe(teleportContainer.childNodes[4])
expect((teleport2.nodes as Node[])[0]).toBe(
teleportContainer.childNodes[5],
)
expect(teleport2.targetAnchor).toBe(teleportContainer.childNodes[7])
expect(container.innerHTML).toBe(
`<!--[-->` +
`<!--teleport start--><!--teleport end-->` +
`<!--teleport start--><!--teleport end-->` +
`<!--]-->`,
)
// event handler
triggerEvent('click', teleportContainer.querySelector('.foo')!)
expect(data.value.fn1).toHaveBeenCalled()
triggerEvent('click', teleportContainer.querySelector('.foo2')!)
expect(data.value.fn2).toHaveBeenCalled()
data.value.msg = 'bar'
await nextTick()
expect(teleportContainer.innerHTML).toBe(
`<!--teleport start anchor-->` +
`<span>bar</span>` +
`<span class="bar"></span>` +
`<!--teleport anchor-->` +
`<!--teleport start anchor-->` +
`<span>bar2</span>` +
`<span class="bar2"></span>` +
`<!--teleport anchor-->`,
)
})
test('disabled', async () => {
const data = ref({
msg: ref('foo'),
fn1: vi.fn(),
fn2: vi.fn(),
})
const code = `
<div>foo</div>
<teleport to="#teleport3" disabled="true">
<span>{{data.msg}}</span>
<span :class="data.msg" @click="data.fn1"></span>
</teleport>
<div :class="data.msg + 2" @click="data.fn2">bar</div>
`
const SSRComp = compileVaporComponent(code, data, undefined, true)
const teleportContainer = document.createElement('div')
teleportContainer.id = 'teleport3'
const ctx = {} as any
const mainHtml = await VueServerRenderer.renderToString(
runtimeDom.createSSRApp(SSRComp),
ctx,
)
expect(mainHtml).toBe(
`<!--[-->` +
`<div>foo</div>` +
`<!--teleport start-->` +
`<span>foo</span>` +
`<span class="foo"></span>` +
`<!--teleport end-->` +
`<div class="foo2">bar</div>` +
`<!--]-->`,
)
const teleportHtml = ctx.teleports!['#teleport3']
expect(teleportHtml).toMatchInlineSnapshot(
`"<!--teleport start anchor--><!--teleport anchor-->"`,
)
teleportContainer.innerHTML = teleportHtml
document.body.appendChild(teleportContainer)
const { block, container } = await mountWithHydration(
mainHtml,
code,
data,
)
const blocks = block as any[]
expect(blocks[0]).toBe(container.childNodes[1])
const teleport = blocks[1] as TeleportFragment
expect((teleport.nodes as Node[])[0]).toBe(container.childNodes[3])
expect((teleport.nodes as Node[])[1]).toBe(container.childNodes[4])
expect(teleport.anchor).toBe(container.childNodes[5])
expect(teleport.target).toBe(teleportContainer)
expect(teleport.targetStart).toBe(teleportContainer.childNodes[0])
expect(teleport.targetAnchor).toBe(teleportContainer.childNodes[1])
expect(blocks[2]).toBe(container.childNodes[6])
expect(container.innerHTML).toBe(
`<!--[-->` +
`<div>foo</div>` +
`<!--teleport start-->` +
`<span>foo</span>` +
`<span class="foo"></span>` +
`<!--teleport end-->` +
`<div class="foo2">bar</div>` +
`<!--]-->`,
)
// event handler
triggerEvent('click', container.querySelector('.foo')!)
expect(data.value.fn1).toHaveBeenCalled()
triggerEvent('click', container.querySelector('.foo2')!)
expect(data.value.fn2).toHaveBeenCalled()
data.value.msg = 'bar'
await nextTick()
expect(container.innerHTML).toBe(
`<!--[-->` +
`<div>foo</div>` +
`<!--teleport start-->` +
`<span>bar</span>` +
`<span class="bar"></span>` +
`<!--teleport end-->` +
`<div class="bar2">bar</div>` +
`<!--]-->`,
)
})
test('disabled + as component root', async () => {
const { container } = await mountWithHydration(
`<!--[-->` +
`<div>Parent fragment</div>` +
`<!--teleport start--><div>Teleport content</div><!--teleport end-->` +
`<!--]-->`,
`
<div>Parent fragment</div>
<teleport to="body" disabled>
<div>Teleport content</div>
</teleport>
`,
)
expect(container.innerHTML).toBe(
`<!--[-->` +
`<div>Parent fragment</div>` +
`<!--teleport start-->` +
`<div>Teleport content</div>` +
`<!--teleport end-->` +
`<!--]-->`,
)
expect(`mismatch`).not.toHaveBeenWarned()
})
test('as component root', async () => {
const teleportContainer = document.createElement('div')
teleportContainer.id = 'teleport4'
teleportContainer.innerHTML = `<!--teleport start anchor-->hello<!--teleport anchor-->`
document.body.appendChild(teleportContainer)
const { block, container } = await mountWithHydration(
'<!--teleport start--><!--teleport end-->',
`<components.Wrapper></components.Wrapper>`,
undefined,
{
Wrapper: compileVaporComponent(
`<teleport to="#teleport4">hello</teleport>`,
),
},
)
const teleport = (block as VaporComponentInstance)
.block as TeleportFragment
expect(teleport.anchor).toBe(container.childNodes[1])
expect(teleport.target).toBe(teleportContainer)
expect(teleport.targetStart).toBe(teleportContainer.childNodes[0])
expect(teleport.nodes).toBe(teleportContainer.childNodes[1])
expect(teleport.targetAnchor).toBe(teleportContainer.childNodes[2])
})
test('nested', async () => {
const teleportContainer = document.createElement('div')
teleportContainer.id = 'teleport5'
teleportContainer.innerHTML =
`<!--teleport start anchor-->` +
`<!--teleport start--><!--teleport end-->` +
`<!--teleport anchor-->` +
`<!--teleport start anchor-->` +
`<div>child</div>` +
`<!--teleport anchor-->`
document.body.appendChild(teleportContainer)
const { block, container } = await mountWithHydration(
'<!--teleport start--><!--teleport end-->',
`<teleport to="#teleport5">
<teleport to="#teleport5"><div>child</div></teleport>
</teleport>`,
)
const teleport = block as TeleportFragment
expect(teleport.anchor).toBe(container.childNodes[1])
expect(teleport.targetStart).toBe(teleportContainer.childNodes[0])
expect(teleport.targetAnchor).toBe(teleportContainer.childNodes[3])
const childTeleport = teleport.nodes as TeleportFragment
expect(childTeleport.anchor).toBe(teleportContainer.childNodes[2])
expect(childTeleport.targetStart).toBe(teleportContainer.childNodes[4])
expect(childTeleport.targetAnchor).toBe(teleportContainer.childNodes[6])
expect(childTeleport.nodes).toBe(teleportContainer.childNodes[5])
})
test('unmount (full integration)', async () => {
const targetId = 'teleport6'
const data = ref({
toggle: ref(true),
})
const template1 = `<Teleport to="#${targetId}"><span>Teleported Comp1</span></Teleport>`
const Comp1 = compileVaporComponent(template1)
const SSRComp1 = compileVaporComponent(
template1,
undefined,
undefined,
true,
)
const template2 = `<div>Comp2</div>`
const Comp2 = compileVaporComponent(template2)
const SSRComp2 = compileVaporComponent(
template2,
undefined,
undefined,
true,
)
const appCode = `
<div>
<components.Comp1 v-if="data.toggle"/>
<components.Comp2 v-else/>
</div>
`
const SSRApp = compileVaporComponent(
appCode,
data,
{
Comp1: SSRComp1,
Comp2: SSRComp2,
},
true,
)
const teleportContainer = document.createElement('div')
teleportContainer.id = targetId
document.body.appendChild(teleportContainer)
const ctx = {} as any
const mainHtml = await VueServerRenderer.renderToString(
runtimeDom.createSSRApp(SSRApp),
ctx,
)
expect(mainHtml).toBe(
'<div><!--teleport start--><!--teleport end--></div>',
)
teleportContainer.innerHTML = ctx.teleports![`#${targetId}`]
const { container } = await mountWithHydration(mainHtml, appCode, data, {
Comp1,
Comp2,
})
expect(container.innerHTML).toBe(
'<div><!--teleport start--><!--teleport end--><!--if--></div>',
)
expect(teleportContainer.innerHTML).toBe(
`<!--teleport start anchor-->` +
`<span>Teleported Comp1</span>` +
`<!--teleport anchor-->`,
)
expect(`mismatch`).not.toHaveBeenWarned()
data.value.toggle = false
await nextTick()
expect(container.innerHTML).toBe('<div><div>Comp2</div><!--if--></div>')
expect(teleportContainer.innerHTML).toBe('')
})
test('unmount (mismatch + full integration)', async () => {
const targetId = 'teleport7'
const data = ref({
toggle: ref(true),
})
const template1 = `<Teleport to="#${targetId}"><span>Teleported Comp1</span></Teleport>`
const Comp1 = compileVaporComponent(template1)
const SSRComp1 = compileVaporComponent(
template1,
undefined,
undefined,
true,
)
const template2 = `<div>Comp2</div>`
const Comp2 = compileVaporComponent(template2)
const SSRComp2 = compileVaporComponent(
template2,
undefined,
undefined,
true,
)
const appCode = `
<div>
<components.Comp1 v-if="data.toggle"/>
<components.Comp2 v-else/>
</div>
`
const SSRApp = compileVaporComponent(
appCode,
data,
{
Comp1: SSRComp1,
Comp2: SSRComp2,
},
true,
)
const teleportContainer = document.createElement('div')
teleportContainer.id = targetId
document.body.appendChild(teleportContainer)
const mainHtml = await VueServerRenderer.renderToString(
runtimeDom.createSSRApp(SSRApp),
)
expect(mainHtml).toBe(
'<div><!--teleport start--><!--teleport end--></div>',
)
expect(teleportContainer.innerHTML).toBe('')
const { container } = await mountWithHydration(mainHtml, appCode, data, {
Comp1,
Comp2,
})
expect(container.innerHTML).toBe(
'<div><!--teleport start--><!--teleport end--><!--if--></div>',
)
expect(teleportContainer.innerHTML).toBe(`<span>Teleported Comp1</span>`)
expect(`Hydration children mismatch`).toHaveBeenWarned()
data.value.toggle = false
await nextTick()
expect(container.innerHTML).toBe('<div><div>Comp2</div><!--if--></div>')
expect(teleportContainer.innerHTML).toBe('')
})
test('target change (mismatch + full integration)', async () => {
const targetId1 = 'teleport8-1'
const targetId2 = 'teleport8-2'
const data = ref({
target: ref(targetId1),
msg: ref('foo'),
})
const template = `<Teleport :to="'#' + data.target"><span>{{data.msg}}</span></Teleport>`
const Comp = compileVaporComponent(template, data)
const SSRComp = compileVaporComponent(template, data, undefined, true)
const teleportContainer1 = document.createElement('div')
teleportContainer1.id = targetId1
const teleportContainer2 = document.createElement('div')
teleportContainer2.id = targetId2
document.body.appendChild(teleportContainer1)
document.body.appendChild(teleportContainer2)
// server render
const mainHtml = await VueServerRenderer.renderToString(
runtimeDom.createSSRApp(SSRComp),
)
expect(mainHtml).toBe(`<!--teleport start--><!--teleport end-->`)
expect(teleportContainer1.innerHTML).toBe('')
expect(teleportContainer2.innerHTML).toBe('')
// hydrate
const { container } = await mountWithHydration(mainHtml, template, data, {
Comp,
})
expect(container.innerHTML).toBe(
`<!--teleport start--><!--teleport end-->`,
)
expect(teleportContainer1.innerHTML).toBe(`<span>foo</span>`)
expect(teleportContainer2.innerHTML).toBe('')
expect(`Hydration children mismatch`).toHaveBeenWarned()
data.value.target = targetId2
data.value.msg = 'bar'
await nextTick()
expect(container.innerHTML).toBe(
`<!--teleport start--><!--teleport end-->`,
)
expect(teleportContainer1.innerHTML).toBe('')
expect(teleportContainer2.innerHTML).toBe(`<span>bar</span>`)
})
test('with disabled teleport + undefined target', async () => {
const data = ref({
msg: ref('foo'),
})
const { container } = await mountWithHydration(
'<!--teleport start--><span>foo</span><!--teleport end-->',
`<teleport :to="undefined" :disabled="true">
<span>{{data.msg}}</span>
</teleport>`,
data,
)
expect(container.innerHTML).toBe(
`<!--teleport start--><span>foo</span><!--teleport end-->`,
)
data.value.msg = 'bar'
await nextTick()
expect(container.innerHTML).toBe(
`<!--teleport start--><span>bar</span><!--teleport end-->`,
)
})
})
describe.todo('Suspense')
})

View File

@ -27,8 +27,9 @@ import { renderEffect } from './renderEffect'
import { VaporVForFlags } from '../../shared/src/vaporFlags'
import {
advanceHydrationNode,
currentHydrationNode,
isComment,
isHydrating,
locateFragmentEndAnchor,
locateHydrationNode,
setCurrentHydrationNode,
} from './dom/hydration'
@ -145,12 +146,12 @@ export const createFor = (
findLastChild(newBlocks[newLength - 1].nodes)!.nextSibling,
)
}
parentAnchor = locateFragmentEndAnchor()!
if (__DEV__) {
if (!parentAnchor) {
throw new Error(`v-for fragment anchor node was not found.`)
}
;(parentAnchor as Comment).data = 'for'
parentAnchor =
newLength === 0
? currentHydrationNode!.nextSibling!
: currentHydrationNode!
if (!parentAnchor || (parentAnchor && !isComment(parentAnchor, ']'))) {
throw new Error(`v-for fragment anchor node was not found.`)
}
}
} else {

View File

@ -19,7 +19,8 @@ import {
type VaporFragment,
isFragment,
} from './fragment'
import { child } from './dom/node'
import { _child } from './dom/node'
import { TeleportFragment } from './components/Teleport'
export interface TransitionOptions {
$key?: any
@ -68,11 +69,11 @@ export function isValidBlock(block: Block): boolean {
export function insert(
block: Block,
parent: ParentNode & { $anchor?: Node | null },
parent: ParentNode & { $prependAnchor?: Node | null },
anchor: Node | null | 0 = null, // 0 means prepend
parentSuspense?: any, // TODO Suspense
): void {
anchor = anchor === 0 ? child(parent) : anchor
anchor = anchor === 0 ? parent.$prependAnchor || _child(parent) : anchor
if (block instanceof Node) {
if (!isHydrating) {
// only apply transition on Element nodes
@ -182,12 +183,12 @@ export function normalizeBlock(block: Block): Node[] {
} else if (isVaporComponent(block)) {
nodes.push(...normalizeBlock(block.block!))
} else {
if (block.getNodes) {
nodes.push(...normalizeBlock(block.getNodes()))
if (block instanceof TeleportFragment) {
nodes.push(block.placeholder!, block.anchor!)
} else {
nodes.push(...normalizeBlock(block.nodes))
block.anchor && nodes.push(block.anchor)
}
block.anchor && nodes.push(block.anchor)
}
return nodes
}

View File

@ -52,7 +52,7 @@ import {
resolveDynamicProps,
setupPropsValidation,
} from './componentProps'
import { renderEffect } from './renderEffect'
import { type RenderEffect, renderEffect } from './renderEffect'
import { emit, normalizeEmitsOptions } from './componentEmits'
import { setDynamicProps } from './dom/prop'
import {
@ -73,7 +73,7 @@ import {
locateHydrationNode,
setCurrentHydrationNode,
} from './dom/hydration'
import { isVaporTeleport } from './components/Teleport'
import { type TeleportFragment, isVaporTeleport } from './components/Teleport'
import {
insertionAnchor,
insertionParent,
@ -445,8 +445,9 @@ export class VaporComponentInstance implements GenericComponentInstance {
setupState?: Record<string, any>
devtoolsRawSetupState?: any
hmrRerender?: () => void
hmrRerenderEffects?: (() => void)[]
hmrReload?: (newComp: VaporComponent) => void
renderEffects?: RenderEffect[]
parentTeleport?: TeleportFragment | null
propsOptions?: NormalizedPropsOptions
emitsOptions?: ObjectEmitsOptions | null
isSingleRoot?: boolean

View File

@ -1,6 +1,8 @@
import {
MismatchTypes,
type TeleportProps,
currentInstance,
type TeleportTargetElement,
isMismatchAllowed,
isTeleportDeferred,
isTeleportDisabled,
queuePostFlushCb,
@ -12,16 +14,21 @@ import { createComment, createTextNode, querySelector } from '../dom/node'
import {
type LooseRawProps,
type LooseRawSlots,
type VaporComponentInstance,
isVaporComponent,
} from '../component'
import { rawPropsProxyHandlers } from '../componentProps'
import { renderEffect } from '../renderEffect'
import { extend, isArray } from '@vue/shared'
import { VaporFragment } from '../fragment'
const instanceToTeleportMap: WeakMap<VaporComponentInstance, TeleportFragment> =
__DEV__ ? new WeakMap() : (undefined as any)
import {
advanceHydrationNode,
currentHydrationNode,
isComment,
isHydrating,
logMismatchError,
runWithoutHydration,
setCurrentHydrationNode,
} from '../dom/hydration'
export const VaporTeleportImpl = {
name: 'VaporTeleport',
@ -29,102 +36,90 @@ export const VaporTeleportImpl = {
__vapor: true,
process(props: LooseRawProps, slots: LooseRawSlots): TeleportFragment {
const frag = new TeleportFragment()
const updateChildrenEffect = renderEffect(() =>
frag.updateChildren(slots.default && (slots.default as BlockFn)()),
)
const updateEffect = renderEffect(() => {
// access the props to trigger tracking
frag.props = extend(
{},
new Proxy(props, rawPropsProxyHandlers) as any as TeleportProps,
)
frag.update()
})
if (__DEV__) {
// used in `normalizeBlock` to get nodes of TeleportFragment during
// HMR updates. returns empty array if content is mounted in target
// container to prevent incorrect parent node lookup.
frag.getNodes = () => {
return frag.parent !== frag.currentParent ? [] : frag.nodes
}
// for HMR rerender
const instance = currentInstance as VaporComponentInstance
;(
instance!.hmrRerenderEffects || (instance!.hmrRerenderEffects = [])
).push(() => {
// remove the teleport content
frag.remove()
// stop effects
updateChildrenEffect.stop()
updateEffect.stop()
})
// for HMR reload
const nodes = frag.nodes
if (isVaporComponent(nodes)) {
instanceToTeleportMap.set(nodes, frag)
} else if (isArray(nodes)) {
nodes.forEach(
node =>
isVaporComponent(node) && instanceToTeleportMap.set(node, frag),
)
}
}
return frag
return new TeleportFragment(props, slots)
},
}
export class TeleportFragment extends VaporFragment {
anchor?: Node
private rawProps?: LooseRawProps
private resolvedProps?: TeleportProps
private rawSlots?: LooseRawSlots
target?: ParentNode | null
targetAnchor?: Node | null
anchor: Node
props?: TeleportProps
targetStart?: Node | null
private targetStart?: Node
private mainAnchor?: Node
private placeholder?: Node
private mountContainer?: ParentNode | null
private mountAnchor?: Node | null
placeholder?: Node
mountContainer?: ParentNode | null
mountAnchor?: Node | null
constructor() {
constructor(props: LooseRawProps, slots: LooseRawSlots) {
super([])
this.anchor = createTextNode()
}
this.rawProps = props
this.rawSlots = slots
this.anchor = isHydrating
? undefined
: __DEV__
? createComment('teleport end')
: createTextNode()
get currentParent(): ParentNode {
return (this.mountContainer || this.parent)!
}
renderEffect(() => {
// access the props to trigger tracking
this.resolvedProps = extend(
{},
new Proxy(
this.rawProps!,
rawPropsProxyHandlers,
) as any as TeleportProps,
)
this.handlePropsUpdate()
})
get currentAnchor(): Node | null {
return this.mountAnchor || this.anchor
if (!isHydrating) {
this.initChildren()
}
}
get parent(): ParentNode | null {
return this.anchor && this.anchor.parentNode
return this.anchor ? this.anchor.parentNode : null
}
updateChildren(children: Block): void {
private initChildren(): void {
renderEffect(() => {
this.handleChildrenUpdate(
this.rawSlots!.default && (this.rawSlots!.default as BlockFn)(),
)
})
if (__DEV__) {
const nodes = this.nodes
if (isVaporComponent(nodes)) {
nodes.parentTeleport = this
} else if (isArray(nodes)) {
nodes.forEach(
node => isVaporComponent(node) && (node.parentTeleport = this),
)
}
}
}
private handleChildrenUpdate(children: Block): void {
// not mounted yet
if (!this.parent) {
if (!this.parent || isHydrating) {
this.nodes = children
return
}
// teardown previous nodes
remove(this.nodes, this.currentParent)
remove(this.nodes, this.mountContainer!)
// mount new nodes
insert((this.nodes = children), this.currentParent, this.currentAnchor)
insert((this.nodes = children), this.mountContainer!, this.mountAnchor!)
}
update(): void {
private handlePropsUpdate(): void {
// not mounted yet
if (!this.parent) return
if (!this.parent || isHydrating) return
const mount = (parent: ParentNode, anchor: Node | null) => {
insert(
@ -136,7 +131,7 @@ export class TeleportFragment extends VaporFragment {
const mountToTarget = () => {
const target = (this.target = resolveTeleportTarget(
this.props!,
this.resolvedProps!,
querySelector,
))
if (target) {
@ -161,12 +156,12 @@ export class TeleportFragment extends VaporFragment {
}
// mount into main container
if (isTeleportDisabled(this.props!)) {
mount(this.parent, this.mainAnchor!)
if (isTeleportDisabled(this.resolvedProps!)) {
mount(this.parent, this.anchor!)
}
// mount into target container
else {
if (isTeleportDeferred(this.props!)) {
if (isTeleportDeferred(this.resolvedProps!)) {
queuePostFlushCb(mountToTarget)
} else {
mountToTarget()
@ -175,20 +170,21 @@ export class TeleportFragment extends VaporFragment {
}
insert = (container: ParentNode, anchor: Node | null): void => {
if (isHydrating) return
// insert anchors in the main view
this.placeholder = __DEV__
? createComment('teleport start')
: createTextNode()
this.mainAnchor = __DEV__ ? createComment('teleport end') : createTextNode()
insert(this.placeholder, container, anchor)
insert(this.mainAnchor, container, anchor)
this.update()
insert(this.anchor!, container, anchor)
this.handlePropsUpdate()
}
remove = (parent: ParentNode | undefined = this.parent!): void => {
// remove nodes
if (this.nodes) {
remove(this.nodes, this.currentParent)
remove(this.nodes, this.mountContainer!)
this.nodes = []
}
@ -200,19 +196,99 @@ export class TeleportFragment extends VaporFragment {
this.targetAnchor = undefined
}
if (this.anchor) {
remove(this.anchor, this.anchor.parentNode!)
this.anchor = undefined
}
if (this.placeholder) {
remove(this.placeholder!, parent)
this.placeholder = undefined
remove(this.mainAnchor!, parent)
this.mainAnchor = undefined
}
this.mountContainer = undefined
this.mountAnchor = undefined
}
private hydrateDisabledTeleport(targetNode: Node | null): void {
let nextNode = this.placeholder!.nextSibling!
setCurrentHydrationNode(nextNode)
this.mountAnchor = this.anchor = locateTeleportEndAnchor(nextNode)!
this.mountContainer = this.anchor.parentNode
this.targetStart = targetNode
this.targetAnchor = targetNode && targetNode.nextSibling
this.initChildren()
}
private mount(target: Node): void {
target.appendChild((this.targetStart = createTextNode('')))
target.appendChild(
(this.mountAnchor = this.targetAnchor = createTextNode('')),
)
if (!isMismatchAllowed(target as Element, MismatchTypes.CHILDREN)) {
if (__DEV__ || __FEATURE_PROD_HYDRATION_MISMATCH_DETAILS__) {
warn(
`Hydration children mismatch on`,
target,
`\nServer rendered element contains fewer child nodes than client nodes.`,
)
}
logMismatchError()
}
runWithoutHydration(this.initChildren.bind(this))
}
hydrate = (): void => {
// TODO
const target = (this.target = resolveTeleportTarget(
this.resolvedProps!,
querySelector,
))
const disabled = isTeleportDisabled(this.resolvedProps!)
this.placeholder = currentHydrationNode!
if (target) {
const targetNode =
(target as TeleportTargetElement)._lpa || target.firstChild
if (disabled) {
this.hydrateDisabledTeleport(targetNode)
} else {
this.anchor = locateTeleportEndAnchor()!
this.mountContainer = target
let targetAnchor = targetNode
while (targetAnchor) {
if (targetAnchor && targetAnchor.nodeType === 8) {
if ((targetAnchor as Comment).data === 'teleport start anchor') {
this.targetStart = targetAnchor
} else if ((targetAnchor as Comment).data === 'teleport anchor') {
this.mountAnchor = this.targetAnchor = targetAnchor
;(target as TeleportTargetElement)._lpa =
this.targetAnchor && this.targetAnchor.nextSibling
break
}
}
targetAnchor = targetAnchor.nextSibling
}
if (targetNode) {
setCurrentHydrationNode(targetNode.nextSibling)
}
// if the HTML corresponding to Teleport is not embedded in the
// correct position on the final page during SSR. the targetAnchor will
// always be null, we need to manually add targetAnchor to ensure
// Teleport it can properly unmount or move
if (!this.targetAnchor) {
this.mount(target)
} else {
this.initChildren()
}
}
} else if (disabled) {
this.hydrateDisabledTeleport(currentHydrationNode!)
}
advanceHydrationNode(this.anchor!)
}
}
@ -222,24 +298,14 @@ export function isVaporTeleport(
return value === VaporTeleportImpl
}
/**
* dev only
* during root component HMR reload, since the old component will be unmounted
* and a new one will be mounted, we need to update the teleport's nodes
* to ensure they are up to date.
*/
export function handleTeleportRootComponentHmrReload(
instance: VaporComponentInstance,
newInstance: VaporComponentInstance,
): void {
const teleport = instanceToTeleportMap.get(instance)
if (teleport) {
instanceToTeleportMap.set(newInstance, teleport)
if (teleport.nodes === instance) {
teleport.nodes = newInstance
} else if (isArray(teleport.nodes)) {
const i = teleport.nodes.indexOf(instance)
if (i !== -1) teleport.nodes[i] = newInstance
function locateTeleportEndAnchor(
node: Node = currentHydrationNode!,
): Node | null {
while (node) {
if (isComment(node, 'teleport end')) {
return node
}
node = node.nextSibling as Node
}
return null
}

View File

@ -1,19 +1,19 @@
import { MismatchTypes, isMismatchAllowed, warn } from '@vue/runtime-dom'
import {
type ChildItem,
incrementIndexOffset,
insertionAnchor,
insertionParent,
resetInsertionState,
setInsertionState,
} from '../insertionState'
import {
_child,
_next,
child,
createElement,
createTextNode,
disableHydrationNodeLookup,
enableHydrationNodeLookup,
locateChildByLogicalIndex,
parentNode,
} from './node'
import { remove } from '../block'
@ -22,6 +22,15 @@ const isHydratingStack = [] as boolean[]
export let isHydrating = false
export let currentHydrationNode: Node | null = null
export function runWithoutHydration(fn: () => any): any {
try {
isHydrating = false
return fn()
} finally {
isHydrating = true
}
}
let isOptimized = false
function performHydration<T>(
@ -37,12 +46,9 @@ function performHydration<T>(
;(Node.prototype as any).$pns = undefined
;(Node.prototype as any).$uc = undefined
;(Node.prototype as any).$idx = undefined
;(Node.prototype as any).$children = undefined
;(Node.prototype as any).$idxMap = undefined
;(Node.prototype as any).$prevDynamicCount = undefined
;(Node.prototype as any).$anchorCount = undefined
;(Node.prototype as any).$appendIndex = undefined
;(Node.prototype as any).$indexOffset = undefined
isOptimized = true
}
@ -120,7 +126,6 @@ function adoptTemplateImpl(node: Node, template: string): Node | null {
) {
const parent = parentNode(node)!
node = parent.insertBefore(createTextNode(), node)
incrementIndexOffset(parent)
break
}
}
@ -143,19 +148,16 @@ function adoptTemplateImpl(node: Node, template: string): Node | null {
function locateHydrationNodeImpl(): void {
let node: Node | null
let idxMap: number[] | undefined
if (insertionAnchor !== undefined && (idxMap = insertionParent!.$idxMap)) {
if (insertionAnchor !== undefined) {
const {
$prevDynamicCount: prevDynamicCount = 0,
$appendIndex: appendIndex,
$indexOffset: indexOffset = 0,
$anchorCount: anchorCount = 0,
} = insertionParent!
// prepend
if (insertionAnchor === 0) {
// use prevDynamicCount as logical index to locate the hydration node
const realIndex = idxMap![prevDynamicCount] + indexOffset
node = insertionParent!.childNodes[realIndex]
node = locateChildByLogicalIndex(insertionParent!, prevDynamicCount)!
}
// insert
else if (insertionAnchor instanceof Node) {
@ -166,11 +168,13 @@ function locateHydrationNodeImpl(): void {
// consecutive insert operations locate the correct hydration node.
let { $idx, $uc: usedCount } = insertionAnchor as ChildItem
if (usedCount !== undefined) {
const realIndex = idxMap![$idx + usedCount + 1] + indexOffset
node = insertionParent!.childNodes[realIndex]
node = locateChildByLogicalIndex(
insertionParent!,
($idx || 0) + usedCount + 1,
)!
usedCount++
} else {
node = insertionAnchor
insertionParent!.$lastLogicalChild = node = insertionAnchor
// first use of this anchor: it doesn't consume the next child
// so we track unique anchor appearances for later offset correction
insertionParent!.$anchorCount = anchorCount + 1
@ -180,22 +184,16 @@ function locateHydrationNodeImpl(): void {
}
// append
else {
let realIndex: number
if (appendIndex !== null && appendIndex !== undefined) {
realIndex = idxMap![appendIndex + 1] + indexOffset
node = insertionParent!.childNodes[realIndex]
node = locateChildByLogicalIndex(insertionParent!, appendIndex + 1)!
} else {
if (insertionAnchor === null) {
// insertionAnchor is null, indicates no previous static nodes
// use the first child as hydration node
realIndex = idxMap![0] + indexOffset
node = insertionParent!.childNodes[realIndex]
node = locateChildByLogicalIndex(insertionParent!, 0)!
} else {
// insertionAnchor is a number > 0
// indicates how many static nodes precede the node to append
// use it as index to locate the hydration node
realIndex = idxMap![prevDynamicCount + insertionAnchor] + indexOffset
node = insertionParent!.childNodes[realIndex]
node = locateChildByLogicalIndex(
insertionParent!,
prevDynamicCount + insertionAnchor,
)!
}
}
insertionParent!.$appendIndex = (node as ChildItem).$idx
@ -246,15 +244,6 @@ export function locateEndAnchor(
return null
}
export function locateFragmentEndAnchor(label: string = ']'): Comment | null {
let node = currentHydrationNode!
while (node) {
if (isComment(node, label)) return node
node = node.nextSibling!
}
return null
}
function handleMismatch(node: Node, template: string): Node {
if (!isMismatchAllowed(node.parentElement!, MismatchTypes.CHILDREN)) {
;(__DEV__ || __FEATURE_PROD_HYDRATION_MISMATCH_DETAILS__) &&
@ -297,7 +286,7 @@ function handleMismatch(node: Node, template: string): Node {
// element node
const t = createElement('template') as HTMLTemplateElement
t.innerHTML = template
const newNode = child(t.content).cloneNode(true) as Element
const newNode = _child(t.content).cloneNode(true) as Element
newNode.innerHTML = (node as Element).innerHTML
Array.from((node as Element).attributes).forEach(attr => {
newNode.setAttribute(attr.name, attr.value)

View File

@ -1,6 +1,7 @@
/* @__NO_SIDE_EFFECTS__ */
import type { ChildItem, InsertionParent } from '../insertionState'
import { isComment, locateEndAnchor } from './hydration'
export function createElement(tagName: string): HTMLElement {
return document.createElement(tagName)
@ -30,14 +31,14 @@ export function parentNode(node: Node): ParentNode | null {
const _txt: typeof _child = _child
/**
* Hydration-specific version of `child`.
* Hydration-specific version of `txt`.
*/
/* @__NO_SIDE_EFFECTS__ */
const __txt: typeof __child = (node: ParentNode): Node => {
const __txt = (node: ParentNode): Node => {
let n = node.firstChild!
// since SSR doesn't generate whitespace placeholder text nodes, if firstChild
// is null, manually insert a text node as the first child
// since SSR doesn't generate blank text nodes,
// manually insert a text node as the first child
if (!n) {
return node.appendChild(createTextNode())
}
@ -47,74 +48,44 @@ const __txt: typeof __child = (node: ParentNode): Node => {
/* @__NO_SIDE_EFFECTS__ */
export function _child(node: InsertionParent): Node {
const children = node.$children
return children ? children[0] : node.firstChild!
return node.firstChild!
}
/**
* Hydration-specific version of `child`.
*/
/* @__NO_SIDE_EFFECTS__ */
export function __child(node: ParentNode): Node {
return __nthChild(node, 0)!
export function __child(node: ParentNode, logicalIndex: number = 0): Node {
return locateChildByLogicalIndex(node as InsertionParent, logicalIndex)!
}
/* @__NO_SIDE_EFFECTS__ */
export function _nthChild(node: InsertionParent, i: number): Node {
const children = node.$children
return children ? children[i] : node.childNodes[i]
return node.childNodes[i]
}
/**
* Hydration-specific version of `nthChild`.
*/
/* @__NO_SIDE_EFFECTS__ */
export function __nthChild(node: Node, i: number): Node {
const parent = node as InsertionParent
if (parent.$idxMap) {
const {
$prevDynamicCount: prevDynamicCount = 0,
$anchorCount: anchorCount = 0,
$idxMap: idxMap,
$indexOffset: indexOffset = 0,
} = parent
// prevDynamicCount tracks how many dynamic nodes have been processed
// so far (prepend/insert/append).
// For anchor-based insert, the first time an anchor is used we adopt the
// anchor node itself and do NOT consume the next child in `idxMap`,
// yet prevDynamicCount is still incremented. This overcounts the base
// offset by 1 per unique anchor that has appeared.
// anchorCount equals the number of unique anchors seen, so we
// subtract it to neutralize those "first-use doesn't consume" cases:
// base = prevDynamicCount - anchorCount
// Then index from this base: idxMap[base + i] + indexOffset.
const logicalIndex = prevDynamicCount - anchorCount + i
const realIndex = idxMap[logicalIndex] + indexOffset
return node.childNodes[realIndex]
}
return node.childNodes[i]
export function __nthChild(node: Node, logicalIndex: number): Node {
return locateChildByLogicalIndex(node as InsertionParent, logicalIndex)!
}
/* @__NO_SIDE_EFFECTS__ */
export function _next(node: Node): Node {
const children = (node.parentNode! as InsertionParent).$children
return children ? children[(node as ChildItem).$idx + 1] : node.nextSibling!
return node.nextSibling!
}
/**
* Hydration-specific version of `next`.
*/
/* @__NO_SIDE_EFFECTS__ */
export function __next(node: Node): Node {
const parent = node.parentNode! as InsertionParent
if (parent.$idxMap) {
const { $idxMap: idxMap, $indexOffset: indexOffset = 0 } = parent
const { $idx, $uc: usedCount = 0 } = node as ChildItem
const logicalIndex = $idx + usedCount + 1
const realIndex = idxMap[logicalIndex] + indexOffset
return node.parentNode!.childNodes[realIndex]
}
return node.nextSibling!
export function __next(node: Node, logicalIndex: number): Node {
return locateChildByLogicalIndex(
node.parentNode! as InsertionParent,
logicalIndex,
)!
}
type DelegatedFunction<T extends (...args: any[]) => any> = T & {
@ -122,26 +93,26 @@ type DelegatedFunction<T extends (...args: any[]) => any> = T & {
}
/* @__NO_SIDE_EFFECTS__ */
export const txt: DelegatedFunction<typeof _txt> = node => {
return txt.impl(node)
export const txt: DelegatedFunction<typeof _txt> = (...args) => {
return txt.impl(...args)
}
txt.impl = _child
txt.impl = _txt
/* @__NO_SIDE_EFFECTS__ */
export const child: DelegatedFunction<typeof _child> = node => {
return child.impl(node)
export const child: DelegatedFunction<typeof _child> = (...args) => {
return child.impl(...args)
}
child.impl = _child
/* @__NO_SIDE_EFFECTS__ */
export const next: DelegatedFunction<typeof _next> = node => {
return next.impl(node)
export const next: DelegatedFunction<typeof _next> = (...args) => {
return next.impl(...args)
}
next.impl = _next
/* @__NO_SIDE_EFFECTS__ */
export const nthChild: DelegatedFunction<typeof _nthChild> = (node, i) => {
return nthChild.impl(node, i)
export const nthChild: DelegatedFunction<typeof _nthChild> = (...args) => {
return nthChild.impl(...args)
}
nthChild.impl = _nthChild
@ -155,9 +126,9 @@ nthChild.impl = _nthChild
*/
export function enableHydrationNodeLookup(): void {
txt.impl = __txt
child.impl = __child
next.impl = __next
nthChild.impl = __nthChild
child.impl = __child as typeof _child
next.impl = __next as typeof _next
nthChild.impl = __nthChild as any as typeof _nthChild
}
export function disableHydrationNodeLookup(): void {
@ -166,3 +137,29 @@ export function disableHydrationNodeLookup(): void {
next.impl = _next
nthChild.impl = _nthChild
}
export function locateChildByLogicalIndex(
node: InsertionParent,
logicalIndex: number,
): Node | null {
let child = (node.$lastLogicalChild || node.firstChild) as ChildItem
let fromIndex = child.$idx || 0
while (child) {
if (fromIndex === logicalIndex) {
child.$idx = logicalIndex
return (node.$lastLogicalChild = child)
}
child = (
isComment(child, '[')
? // fragment start: jump to the node after the matching end anchor
locateEndAnchor(child)!.nextSibling
: child.nextSibling
) as ChildItem
fromIndex++
}
return null
}

View File

@ -386,8 +386,7 @@ export function optimizePropertyLookup(): void {
const proto = Element.prototype as any
proto.$transition = undefined
proto.$key = undefined
proto.$evtclick = undefined
proto.$children = undefined
proto.$prependAnchor = proto.$evtclick = undefined
proto.$idx = undefined
proto.$root = false
proto.$html =

View File

@ -1,15 +1,8 @@
import { adoptTemplate, currentHydrationNode, isHydrating } from './hydration'
import { child, createElement, createTextNode } from './node'
import { _child, createElement, createTextNode } from './node'
let t: HTMLTemplateElement
export let currentTemplateFn: (Function & { $idxMap?: number[] }) | undefined =
undefined
export function resetTemplateFn(): void {
currentTemplateFn = undefined
}
/*! #__NO_SIDE_EFFECTS__ */
export function template(
html: string,
@ -18,8 +11,6 @@ export function template(
let node: Node
const fn = () => {
if (isHydrating) {
currentTemplateFn = fn
// do not cache the adopted node in node because it contains child nodes
// this avoids duplicate rendering of children
const adopted = adoptTemplate(currentHydrationNode!, html)!
@ -34,7 +25,7 @@ export function template(
if (!node) {
t = t || createElement('template')
t.innerHTML = html
node = child(t.content)
node = _child(t.content)
}
const ret = node.cloneNode(true)
if (root) (ret as any).$root = true

View File

@ -9,13 +9,11 @@ import {
isValidBlock,
remove,
} from './block'
import type { TransitionHooks } from '@vue/runtime-dom'
import { type TransitionHooks, queuePostFlushCb } from '@vue/runtime-dom'
import {
advanceHydrationNode,
currentHydrationNode,
isComment,
isHydrating,
locateFragmentEndAnchor,
locateHydrationNode,
} from './dom/hydration'
import {
@ -24,7 +22,6 @@ import {
} from './components/Transition'
import { type VaporComponentInstance, isVaporComponent } from './component'
import { isArray } from '@vue/shared'
import { incrementIndexOffset } from './insertionState'
export class VaporFragment<T extends Block = Block>
implements TransitionOptions
@ -42,7 +39,6 @@ export class VaporFragment<T extends Block = Block>
remove?: (parent?: ParentNode, transitionHooks?: TransitionHooks) => void
fallback?: BlockFn
getNodes?: () => Block
setRef?: (comp: VaporComponentInstance) => void
constructor(nodes: T) {
@ -149,7 +145,7 @@ export class DynamicFragment extends VaporFragment {
// reuse the empty comment node as the anchor for empty if
if (this.anchorLabel === 'if' && isEmpty) {
this.anchor = locateFragmentEndAnchor('')!
this.anchor = currentHydrationNode!
if (!this.anchor) {
throw new Error('Failed to locate if anchor')
} else {
@ -172,7 +168,7 @@ export class DynamicFragment extends VaporFragment {
}
// reuse the vdom fragment end anchor for slots
this.anchor = locateFragmentEndAnchor()!
this.anchor = currentHydrationNode!
if (!this.anchor) {
throw new Error('Failed to locate slot anchor')
} else {
@ -182,13 +178,14 @@ export class DynamicFragment extends VaporFragment {
// create an anchor
const { parentNode, nextSibling } = findLastChild(this)!
parentNode!.insertBefore(
(this.anchor = createComment(this.anchorLabel!)),
nextSibling,
)
// increment index offset since we dynamically inserted a comment node
incrementIndexOffset(parentNode!)
advanceHydrationNode(this.anchor)
queuePostFlushCb(() => {
parentNode!.insertBefore(
(this.anchor = __DEV__
? createComment(this.anchorLabel!)
: createTextNode()),
nextSibling,
)
})
}
}
@ -241,7 +238,7 @@ export function findLastChild(node: Block): Node | undefined | null {
} else if (isVaporComponent(node)) {
return findLastChild(node.block!)
} else {
if (node instanceof DynamicFragment && node.anchor) return node.anchor
if (node.anchor) return node.anchor
return findLastChild(node.nodes!)
}
}

View File

@ -12,19 +12,19 @@ import {
mountComponent,
unmountComponent,
} from './component'
import { handleTeleportRootComponentHmrReload } from './components/Teleport'
import { isArray } from '@vue/shared'
export function hmrRerender(instance: VaporComponentInstance): void {
const normalized = normalizeBlock(instance.block)
const parent = normalized[0].parentNode!
const anchor = normalized[normalized.length - 1].nextSibling
remove(instance.block, parent)
if (instance.hmrRerenderEffects) {
instance.hmrRerenderEffects.forEach(e => e())
instance.hmrRerenderEffects.length = 0
}
const prev = setCurrentInstance(instance)
pushWarningContext(instance)
if (instance.renderEffects) {
instance.renderEffects.forEach(e => e.stop())
instance.renderEffects = []
}
devRender(instance)
popWarningContext()
setCurrentInstance(...prev)
@ -39,7 +39,8 @@ export function hmrReload(
const parent = normalized[0].parentNode!
const anchor = normalized[normalized.length - 1].nextSibling
unmountComponent(instance, parent)
const prev = setCurrentInstance(instance.parent)
const parentInstance = instance.parent as VaporComponentInstance | null
const prev = setCurrentInstance(parentInstance)
const newInstance = createComponent(
newComp,
instance.rawProps,
@ -48,5 +49,59 @@ export function hmrReload(
)
setCurrentInstance(...prev)
mountComponent(newInstance, parent, anchor)
handleTeleportRootComponentHmrReload(instance, newInstance)
updateParentBlockOnHmrReload(parentInstance, instance, newInstance)
updateParentTeleportOnHmrReload(instance, newInstance)
}
/**
* dev only
* update parentInstance.block to ensure that the correct parent and
* anchor are found during parentInstance HMR rerender/reload, as
* `normalizeBlock` relies on the current instance.block
*/
function updateParentBlockOnHmrReload(
parentInstance: VaporComponentInstance | null,
instance: VaporComponentInstance,
newInstance: VaporComponentInstance,
): void {
if (parentInstance) {
if (parentInstance.block === instance) {
parentInstance.block = newInstance
} else if (isArray(parentInstance.block)) {
for (let i = 0; i < parentInstance.block.length; i++) {
if (parentInstance.block[i] === instance) {
parentInstance.block[i] = newInstance
break
}
}
}
}
}
/**
* dev only
* during root component HMR reload, since the old component will be unmounted
* and a new one will be mounted, we need to update the teleport's nodes
* to ensure that the correct parent and anchor are found during parentInstance
* HMR rerender/reload, as `normalizeBlock` relies on the current instance.block
*/
export function updateParentTeleportOnHmrReload(
instance: VaporComponentInstance,
newInstance: VaporComponentInstance,
): void {
const teleport = instance.parentTeleport
if (teleport) {
newInstance.parentTeleport = teleport
if (teleport.nodes === instance) {
teleport.nodes = newInstance
} else if (isArray(teleport.nodes)) {
for (let i = 0; i < teleport.nodes.length; i++) {
if (teleport.nodes[i] === instance) {
teleport.nodes[i] = newInstance
break
}
}
}
}
}

View File

@ -1,5 +1,4 @@
import { isComment, isHydrating } from './dom/hydration'
import { currentTemplateFn, resetTemplateFn } from './dom/template'
import { isHydrating } from './dom/hydration'
export type ChildItem = ChildNode & {
$idx: number
// used count as an anchor
@ -7,20 +6,19 @@ export type ChildItem = ChildNode & {
}
export type InsertionParent = ParentNode & {
$children?: ChildItem[]
$prependAnchor?: Node | null
/**
* hydration-specific properties
*/
// mapping from logical index to real index in childNodes
$idxMap?: number[]
// hydrated dynamic children count so far
$prevDynamicCount?: number
// number of unique insertion anchors that have appeared
$anchorCount?: number
// last append index
$appendIndex?: number | null
// number of dynamically inserted nodes (e.g., comment anchors)
$indexOffset?: number
// last located logical child
$lastLogicalChild?: Node | null
}
export let insertionParent: InsertionParent | undefined
export let insertionAnchor: Node | 0 | undefined | null
@ -31,7 +29,7 @@ export let insertionAnchor: Node | 0 | undefined | null
* insertion on client-side render, and used for node adoption during hydration.
*/
export function setInsertionState(
parent: ParentNode,
parent: ParentNode & { $prependAnchor?: Node | null },
anchor?: Node | 0 | null | number,
): void {
insertionParent = parent
@ -39,119 +37,26 @@ export function setInsertionState(
if (anchor !== undefined) {
if (isHydrating) {
insertionAnchor = anchor as Node
initializeHydrationState(parent)
resetTemplateFn()
// when the setInsertionState is called for the first time, reset $lastLogicalChild,
// in order to reuse it in locateChildByLogicalIndex
if (insertionParent.$prevDynamicCount === undefined) {
insertionParent!.$lastLogicalChild = null
}
} else {
// special handling append anchor value to null
insertionAnchor =
typeof anchor === 'number' && anchor > 0 ? null : (anchor as Node)
cacheTemplateChildren(parent)
// track the first child for potential future use
if (anchor === 0 && !parent.$prependAnchor) {
parent.$prependAnchor = parent.firstChild
}
}
} else {
insertionAnchor = undefined
}
}
function initializeHydrationState(parent: InsertionParent) {
if (!parent.$idxMap) {
const childNodes = parent.childNodes
const len = childNodes.length
// fast path for single child case. use first child as hydration node
// no need to build logical index map
if (
len === 1 ||
(len === 3 &&
isComment(childNodes[0], '[') &&
isComment(childNodes[2], ']'))
) {
insertionAnchor = undefined
return
}
if (currentTemplateFn) {
if (currentTemplateFn.$idxMap) {
const idxMap = (parent.$idxMap = currentTemplateFn.$idxMap)
// set $idx to childNodes
for (let i = 0; i < idxMap.length; i++) {
;(childNodes[idxMap[i]] as ChildItem).$idx = i
}
} else {
parent.$idxMap = currentTemplateFn.$idxMap = buildLogicalIndexMap(
len,
childNodes,
)
}
} else {
parent.$idxMap = buildLogicalIndexMap(len, childNodes)
}
parent.$prevDynamicCount = 0
parent.$anchorCount = 0
parent.$appendIndex = null
parent.$indexOffset = 0
}
}
function buildLogicalIndexMap(len: number, childNodes: NodeListOf<ChildNode>) {
const idxMap = new Array() as number[]
// Build logical index map:
// - static node: map logical index to real index
// - fragment: map logical index to start anchor's real index
let logicalIndex = 0
for (let i = 0; i < len; i++) {
const n = childNodes[i] as ChildItem
n.$idx = logicalIndex
if (n.nodeType === 8) {
const data = (n as any as Comment).data
// vdom fragment
if (data === '[') {
idxMap[logicalIndex++] = i
// find matching end anchor, accounting for nested fragments
let depth = 1
let j = i + 1
for (; j < len; j++) {
const c = childNodes[j] as Comment
if (c.nodeType === 8) {
const d = c.data
if (d === '[') depth++
else if (d === ']') {
depth--
if (depth === 0) break
}
}
}
// jump i to the end anchor
i = j
continue
}
}
idxMap[logicalIndex++] = i
}
return idxMap
}
function cacheTemplateChildren(parent: InsertionParent) {
if (!parent.$children) {
const nodes = parent.childNodes
const len = nodes.length
if (len === 0) return
const children = new Array(len) as ChildItem[]
for (let i = 0; i < len; i++) {
const node = nodes[i] as ChildItem
node.$idx = i
children[i] = node
}
parent.$children = children
}
}
export function resetInsertionState(): void {
insertionParent = insertionAnchor = undefined
}
export function incrementIndexOffset(parent: InsertionParent): void {
if (parent.$indexOffset !== undefined) {
parent.$indexOffset++
}
}

View File

@ -11,7 +11,7 @@ import {
import { type VaporComponentInstance, isVaporComponent } from './component'
import { invokeArrayFns } from '@vue/shared'
class RenderEffect extends ReactiveEffect {
export class RenderEffect extends ReactiveEffect {
i: VaporComponentInstance | null
job: SchedulerJob
updateJob: SchedulerJob
@ -41,6 +41,9 @@ class RenderEffect extends ReactiveEffect {
this.onTrigger = instance.rtg
? e => invokeArrayFns(instance.rtg!, e)
: void 0
// register effect for stopping them during HMR rerender
;(instance.renderEffects || (instance.renderEffects = [])).push(this)
}
job.i = instance
}
@ -82,14 +85,10 @@ class RenderEffect extends ReactiveEffect {
}
}
export function renderEffect(
fn: () => void,
noLifecycle = false,
): RenderEffect {
export function renderEffect(fn: () => void, noLifecycle = false): void {
const effect = new RenderEffect(fn)
if (noLifecycle) {
effect.fn = fn
}
effect.run()
return effect
}

View File

@ -59,7 +59,6 @@ import {
currentHydrationNode,
isComment,
isHydrating,
locateFragmentEndAnchor,
locateHydrationNode,
setCurrentHydrationNode,
hydrateNode as vaporHydrateNode,
@ -75,9 +74,7 @@ const vaporInteropImpl: Omit<
> = {
mount(vnode, container, anchor, parentComponent) {
let selfAnchor = (vnode.el = vnode.anchor = createTextNode())
if (!isHydrating) {
container.insertBefore(selfAnchor, anchor)
}
container.insertBefore(selfAnchor, anchor)
const prev = currentInstance
simpleSetCurrentInstance(parentComponent)
@ -119,12 +116,6 @@ const vaporInteropImpl: Omit<
vnode.transition as VaporTransitionHooks,
)
}
if (isHydrating) {
// insert self anchor after hydration completed to avoid mismatching
;(instance.m || (instance.m = [])).push(() => {
container.insertBefore(selfAnchor, anchor)
})
}
mountComponent(instance, container, selfAnchor)
simpleSetCurrentInstance(prev)
return instance
@ -198,8 +189,7 @@ const vaporInteropImpl: Omit<
const propsRef = (vnode.vs!.ref = shallowRef(vnode.props))
vaporHydrateNode(node, () => {
vnode.vb = slot(new Proxy(propsRef, vaporSlotPropsProxyHandler))
vnode.el = currentHydrationNode!
vnode.anchor = locateFragmentEndAnchor()
vnode.anchor = vnode.el = currentHydrationNode!
if (__DEV__ && !vnode.anchor) {
throw new Error(`Failed to locate slot anchor`)