import type { VNode } from './vnode'
import { devtoolsInitApp, devtoolsUnmountApp } from './devtools'
import { NO, extend, hasOwn, isFunction, isObject } from '@vue/shared'
-import { type TransitionHooks, version } from '.'
+import { type SuspenseBoundary, type TransitionHooks, version } from '.'
import { installAppCompatProperties } from './compat/global'
import type { NormalizedPropsOptions } from './componentProps'
import type { ObjectEmitsOptions } from './componentEmits'
container: any,
anchor: any,
parentComponent: ComponentInternalInstance | null,
+ parentSuspense: SuspenseBoundary | null,
): GenericComponentInstance // VaporComponentInstance
update(n1: VNode, n2: VNode, shouldUpdate: boolean): void
unmount(vnode: VNode, doRemove?: boolean): void
container: any,
anchor: any,
parentComponent: ComponentInternalInstance | null,
+ parentSuspense: SuspenseBoundary | null,
): Node
hydrateSlot(vnode: VNode, node: any): Node
activate(
* @internal
*/
suspense: SuspenseBoundary | null
+ /**
+ * suspense pending batch id
+ * @internal
+ */
+ suspenseId: number
+ /**
+ * @internal
+ */
+ asyncDep: Promise<any> | null
+ /**
+ * @internal
+ */
+ asyncResolved: boolean
/**
* `updateTeleportCssVars`
* For updating css vars on contained teleports
openBlock,
} from '../vnode'
import { ShapeFlags, isArray, isFunction, toNumber } from '@vue/shared'
-import { type ComponentInternalInstance, handleSetupResult } from '../component'
+import type {
+ ComponentInternalInstance,
+ GenericComponentInstance,
+} from '../component'
import type { Slots } from '../componentSlots'
import {
type ElementNamespace,
type RendererElement,
type RendererInternals,
type RendererNode,
- type SetupRenderEffectFn,
queuePostRenderEffect,
} from '../renderer'
import { queuePostFlushCb } from '../scheduler'
import { filterSingleRoot, updateHOCHostEl } from '../componentRenderUtils'
-import {
- assertNumber,
- popWarningContext,
- pushWarningContext,
- warn,
-} from '../warning'
+import { assertNumber, warn } from '../warning'
import { ErrorCodes, handleError } from '../errorHandling'
import { NULL_DYNAMIC_COMPONENT } from '../helpers/resolveAssets'
): void
next(): RendererNode | null
registerDep(
- instance: ComponentInternalInstance,
- setupRenderEffect: SetupRenderEffectFn,
- optimized: boolean,
+ instance: GenericComponentInstance,
+ onResolve: (setupResult: unknown) => void,
): void
unmount(parentSuspense: SuspenseBoundary | null, doRemove?: boolean): void
}
m: move,
um: unmount,
n: next,
- o: { parentNode, remove },
+ o: { parentNode },
} = rendererInternals
// if set `suspensible: true`, set the current suspense as a dep of parent suspense
return suspense.activeBranch && next(suspense.activeBranch)
},
- registerDep(instance, setupRenderEffect, optimized) {
+ registerDep(instance, onResolve) {
const isInPendingSuspense = !!suspense.pendingBranch
if (isInPendingSuspense) {
suspense.deps++
}
- const hydratedEl = instance.vnode.el
+
instance
.asyncDep!.catch(err => {
handleError(err, instance, ErrorCodes.SETUP_FUNCTION)
}
// retry from this component
instance.asyncResolved = true
- const { vnode } = instance
- if (__DEV__) {
- pushWarningContext(vnode)
- }
- handleSetupResult(instance, asyncSetupResult, false)
- if (hydratedEl) {
- // vnode may have been replaced if an update happened before the
- // async dep is resolved.
- vnode.el = hydratedEl
- }
- const placeholder = !hydratedEl && instance.subTree.el
- setupRenderEffect(
- instance,
- vnode,
- // component may have been moved before resolve.
- // if this is not a hydration, instance.subTree will be the comment
- // placeholder.
- parentNode(hydratedEl || instance.subTree.el!)!,
- // anchor will not be used if this is hydration, so only need to
- // consider the comment placeholder case.
- hydratedEl ? null : next(instance.subTree),
- suspense,
- namespace,
- optimized,
- )
- if (placeholder) {
- // clean up placeholder reference
- vnode.placeholder = null
- remove(placeholder)
- }
- updateHOCHostEl(instance, vnode.el)
- if (__DEV__) {
- popWarningContext()
- }
+ onResolve(asyncSetupResult)
+
// only decrease deps count if suspense is not already resolved
if (isInPendingSuspense && --suspense.deps === 0) {
suspense.resolve()
container,
null,
parentComponent,
+ parentSuspense,
)
} else {
mountComponent(
type LifecycleHook,
createComponentInstance,
getComponentPublicInstance,
+ handleSetupResult,
setupComponent,
} from './component'
import {
container,
anchor,
parentComponent,
+ parentSuspense,
)
}
} else {
// setup() is async. This component relies on async logic to be resolved
// before proceeding
if (__FEATURE_SUSPENSE__ && instance.asyncDep) {
- parentSuspense &&
- parentSuspense.registerDep(instance, setupRenderEffect, optimized)
-
+ if (parentSuspense) {
+ const hydratedEl = instance.vnode.el
+ parentSuspense.registerDep(instance, setupResult => {
+ const { vnode } = instance
+ if (__DEV__) {
+ pushWarningContext(vnode)
+ }
+ handleSetupResult(instance, setupResult, false)
+ if (hydratedEl) {
+ // vnode may have been replaced if an update happened before the
+ // async dep is resolved.
+ vnode.el = hydratedEl
+ }
+ const placeholder = !hydratedEl && instance.subTree.el
+ setupRenderEffect(
+ instance,
+ vnode,
+ // component may have been moved before resolve.
+ // if this is not a hydration, instance.subTree will be the comment
+ // placeholder.
+ hostParentNode(hydratedEl || instance.subTree.el!)!,
+ // anchor will not be used if this is hydration, so only need to
+ // consider the comment placeholder case.
+ hydratedEl ? null : getNextHostNode(instance.subTree),
+ parentSuspense,
+ namespace,
+ optimized,
+ )
+ if (placeholder) {
+ // clean up placeholder reference
+ vnode.placeholder = null
+ hostRemove(placeholder)
+ }
+ updateHOCHostEl(instance, vnode.el)
+ if (__DEV__) {
+ popWarningContext()
+ }
+ })
+ }
// Give it a placeholder if this is not hydration
// TODO handle self-defined fallback
if (!initialVNode.el) {
--- /dev/null
+import { nextTick, reactive } from 'vue'
+import { compile, runtimeDom, runtimeVapor } from '../_utils'
+
+describe.todo('VaporSuspense', () => {})
+
+describe('vdom interop', () => {
+ beforeEach(() => {
+ document.body.innerHTML = ''
+ })
+
+ async function testSuspense(
+ code: string,
+ components: Record<string, { code: string; vapor: boolean }> = {},
+ data: any = {},
+ { vapor = false } = {},
+ ) {
+ const clientComponents: any = {}
+ for (const key in components) {
+ const comp = components[key]
+ let code = comp.code
+ const isVaporComp = !!comp.vapor
+ clientComponents[key] = compile(code, data, clientComponents, {
+ vapor: isVaporComp,
+ })
+ }
+
+ const clientComp = compile(code, data, clientComponents, {
+ vapor,
+ })
+
+ const app = (vapor ? runtimeVapor.createVaporApp : runtimeDom.createApp)(
+ clientComp,
+ )
+ app.use(runtimeVapor.vaporInteropPlugin)
+
+ const container = document.createElement('div')
+ document.body.appendChild(container)
+ app.mount(container)
+ return { container }
+ }
+
+ function withAsyncScript(code: string) {
+ return {
+ code: `
+ <script vapor>
+ const data = _data;
+ const components = _components;
+ const p = new Promise(r => setTimeout(r, 5))
+ data.deps.push(p.then(() => Promise.resolve()))
+ await p
+ </script>
+ ${code}
+ `,
+ vapor: true,
+ }
+ }
+
+ test('vdom suspense: render vapor components', async () => {
+ const data = { deps: [] }
+ const { container } = await testSuspense(
+ `<script setup>
+ const components = _components;
+ </script>
+ <template>
+ <Suspense>
+ <components.VaporChild/>
+ <template #fallback>
+ <span>fallback</span>
+ </template>
+ </Suspense>
+ </template>`,
+ {
+ VaporChild: withAsyncScript(`<template><div>hi</div></template>`),
+ },
+ data,
+ )
+
+ expect(container.innerHTML).toBe(`<span>fallback</span>`)
+ expect(data.deps.length).toBe(1)
+ await Promise.all(data.deps)
+ await nextTick()
+ expect(container.innerHTML).toBe(`<div>hi</div>`)
+ })
+
+ test('vdom suspense: nested async vapor components', async () => {
+ const data = { deps: [] }
+ const { container } = await testSuspense(
+ `<script setup>
+ const components = _components;
+ </script>
+ <template>
+ <Suspense>
+ <components.AsyncOuter/>
+ <template #fallback>
+ <span>fallback</span>
+ </template>
+ </Suspense>
+ </template>`,
+ {
+ AsyncOuter: withAsyncScript(
+ `<template><components.AsyncInner/></template>`,
+ ),
+ AsyncInner: withAsyncScript(`<template><div>inner</div></template>`),
+ },
+ data,
+ )
+
+ expect(container.innerHTML).toBe(`<span>fallback</span>`)
+
+ await data.deps[0]
+ await nextTick()
+ expect(container.innerHTML).toBe(`<span>fallback</span>`)
+
+ await Promise.all(data.deps)
+ await nextTick()
+ expect(container.innerHTML).toBe(`<div>inner</div>`)
+ })
+
+ test('vdom suspense: content update before suspense resolve', async () => {
+ const data = reactive({ msg: 'foo', deps: [] })
+ const { container } = await testSuspense(
+ `<script setup>
+ const data = _data;
+ const components = _components;
+ </script>
+ <template>
+ <Suspense>
+ <components.VaporChild/>
+ <template #fallback>
+ <span>fallback {{data.msg}}</span>
+ </template>
+ </Suspense>
+ </template>`,
+ {
+ VaporChild: withAsyncScript(
+ `<template><div>{{data.msg}}</div></template>`,
+ ),
+ },
+ data,
+ )
+
+ expect(container.innerHTML).toBe(`<span>fallback foo</span>`)
+
+ data.msg = 'bar'
+ await nextTick()
+ expect(container.innerHTML).toBe(`<span>fallback bar</span>`)
+
+ await Promise.all(data.deps)
+ await nextTick()
+ expect(container.innerHTML).toBe(`<div>bar</div>`)
+ })
+
+ test('vdom suspense: unmount before suspense resolve', async () => {
+ const data = reactive({ show: true, deps: [] })
+ const { container } = await testSuspense(
+ `<script setup>
+ const data = _data;
+ const components = _components;
+ </script>
+ <template>
+ <Suspense>
+ <components.VaporChild v-if="data.show"/>
+ <template #fallback>
+ <span>fallback</span>
+ </template>
+ </Suspense>
+ </template>`,
+ {
+ VaporChild: withAsyncScript(`<template><div>child</div></template>`),
+ },
+ data,
+ )
+
+ expect(container.innerHTML).toBe(`<span>fallback</span>`)
+
+ data.show = false
+ await nextTick()
+ expect(container.innerHTML).toBe(`<!--v-if-->`)
+
+ await Promise.all(data.deps)
+ await nextTick()
+ expect(container.innerHTML).toBe(`<!--v-if-->`)
+ })
+
+ test('vdom suspense: unmount suspense after resolve', async () => {
+ const data = reactive({ show: true, deps: [] })
+ const { container } = await testSuspense(
+ `<script setup>
+ const data = _data;
+ const components = _components;
+ </script>
+ <template>
+ <Suspense v-if="data.show">
+ <components.VaporChild/>
+ <template #fallback>
+ <span>fallback</span>
+ </template>
+ </Suspense>
+ </template>`,
+ {
+ VaporChild: withAsyncScript(`<template><div>child</div></template>`),
+ },
+ data,
+ )
+
+ expect(container.innerHTML).toBe(`<span>fallback</span>`)
+
+ await Promise.all(data.deps)
+ await nextTick()
+ expect(container.innerHTML).toBe(`<div>child</div>`)
+
+ data.show = false
+ await nextTick()
+ expect(container.innerHTML).toBe(`<!--v-if-->`)
+ })
+
+ test('vdom suspense: unmount suspense before resolve', async () => {
+ const data = reactive({ show: true, deps: [] })
+ const { container } = await testSuspense(
+ `<script setup>
+ const data = _data;
+ const components = _components;
+ </script>
+ <template>
+ <Suspense v-if="data.show">
+ <components.VaporChild/>
+ <template #fallback>
+ <span>fallback</span>
+ </template>
+ </Suspense>
+ </template>`,
+ {
+ VaporChild: withAsyncScript(`<template><div>child</div></template>`),
+ },
+ data,
+ )
+
+ expect(container.innerHTML).toBe(`<span>fallback</span>`)
+
+ data.show = false
+ await nextTick()
+ expect(container.innerHTML).toBe(`<!--v-if-->`)
+
+ await Promise.all(data.deps)
+ await nextTick()
+ expect(container.innerHTML).toBe(`<!--v-if-->`)
+ })
+})
--- /dev/null
+// TODO: Suspense is required to be implemented for this test to pass
+
+// /**
+// * @vitest-environment jsdom
+// */
+// import {
+// type App,
+// Suspense,
+// createApp,
+// defineAsyncComponent,
+// defineComponent,
+// h,
+// onServerPrefetch,
+// useId,
+// } from 'vue'
+// import { renderToString } from '@vue/server-renderer'
+
+// type FactoryRes = [App, Promise<any>[]]
+// type TestCaseFactory = () => FactoryRes | Promise<FactoryRes>
+
+// async function runOnClient(factory: TestCaseFactory) {
+// const [app, deps] = await factory()
+// const root = document.createElement('div')
+// app.mount(root)
+// await Promise.all(deps)
+// await promiseWithDelay(null, 0)
+// return root.innerHTML
+// }
+
+// async function runOnServer(factory: TestCaseFactory) {
+// const [app, _] = await factory()
+// return (await renderToString(app))
+// .replace(/<!--[\[\]]-->/g, '') // remove fragment wrappers
+// .trim()
+// }
+
+// async function getOutput(factory: TestCaseFactory) {
+// const clientResult = await runOnClient(factory)
+// const serverResult = await runOnServer(factory)
+// expect(serverResult).toBe(clientResult)
+// return clientResult
+// }
+
+// function promiseWithDelay(res: any, delay: number) {
+// return new Promise<any>(r => {
+// setTimeout(() => r(res), delay)
+// })
+// }
+
+// const BasicComponentWithUseId = defineComponent({
+// setup() {
+// const id1 = useId()
+// const id2 = useId()
+// return () => [id1, ' ', id2]
+// },
+// })
+
+describe.todo('useId', () => {
+ // test('basic', async () => {
+ // expect(
+ // await getOutput(() => {
+ // const app = createApp(BasicComponentWithUseId)
+ // return [app, []]
+ // }),
+ // ).toBe('v-0 v-1')
+ // })
+ // test('with config.idPrefix', async () => {
+ // expect(
+ // await getOutput(() => {
+ // const app = createApp(BasicComponentWithUseId)
+ // app.config.idPrefix = 'foo'
+ // return [app, []]
+ // }),
+ // ).toBe('foo-0 foo-1')
+ // })
+ // test('async component', async () => {
+ // const factory = (
+ // delay1: number,
+ // delay2: number,
+ // ): ReturnType<TestCaseFactory> => {
+ // const p1 = promiseWithDelay(BasicComponentWithUseId, delay1)
+ // const p2 = promiseWithDelay(BasicComponentWithUseId, delay2)
+ // const AsyncOne = defineAsyncComponent(() => p1)
+ // const AsyncTwo = defineAsyncComponent(() => p2)
+ // const app = createApp({
+ // setup() {
+ // const id1 = useId()
+ // const id2 = useId()
+ // return () => [id1, ' ', id2, ' ', h(AsyncOne), ' ', h(AsyncTwo)]
+ // },
+ // })
+ // return [app, [p1, p2]]
+ // }
+ // const expected =
+ // 'v-0 v-1 ' + // root
+ // 'v-0-0 v-0-1 ' + // inside first async subtree
+ // 'v-1-0 v-1-1' // inside second async subtree
+ // // assert different async resolution order does not affect id stable-ness
+ // expect(await getOutput(() => factory(0, 16))).toBe(expected)
+ // expect(await getOutput(() => factory(16, 0))).toBe(expected)
+ // })
+ // test('serverPrefetch', async () => {
+ // const factory = (
+ // delay1: number,
+ // delay2: number,
+ // ): ReturnType<TestCaseFactory> => {
+ // const p1 = promiseWithDelay(null, delay1)
+ // const p2 = promiseWithDelay(null, delay2)
+ // const SPOne = defineComponent({
+ // async serverPrefetch() {
+ // await p1
+ // },
+ // render() {
+ // return h(BasicComponentWithUseId)
+ // },
+ // })
+ // const SPTwo = defineComponent({
+ // async serverPrefetch() {
+ // await p2
+ // },
+ // render() {
+ // return h(BasicComponentWithUseId)
+ // },
+ // })
+ // const app = createApp({
+ // setup() {
+ // const id1 = useId()
+ // const id2 = useId()
+ // return () => [id1, ' ', id2, ' ', h(SPOne), ' ', h(SPTwo)]
+ // },
+ // })
+ // return [app, [p1, p2]]
+ // }
+ // const expected =
+ // 'v-0 v-1 ' + // root
+ // 'v-0-0 v-0-1 ' + // inside first async subtree
+ // 'v-1-0 v-1-1' // inside second async subtree
+ // // assert different async resolution order does not affect id stable-ness
+ // expect(await getOutput(() => factory(0, 16))).toBe(expected)
+ // expect(await getOutput(() => factory(16, 0))).toBe(expected)
+ // })
+ // test('components with serverPrefetch', async () => {
+ // const factory = (): ReturnType<TestCaseFactory> => {
+ // const SPOne = defineComponent({
+ // setup() {
+ // onServerPrefetch(() => {})
+ // return () => h(BasicComponentWithUseId)
+ // },
+ // })
+ // const SPTwo = defineComponent({
+ // render() {
+ // return h(BasicComponentWithUseId)
+ // },
+ // })
+ // const app = createApp({
+ // setup() {
+ // const id1 = useId()
+ // const id2 = useId()
+ // return () => [id1, ' ', id2, ' ', h(SPOne), ' ', h(SPTwo)]
+ // },
+ // })
+ // return [app, []]
+ // }
+ // const expected =
+ // 'v-0 v-1 ' + // root
+ // 'v-0-0 v-0-1 ' + // inside first async subtree
+ // 'v-2 v-3' // inside second async subtree
+ // // assert different async resolution order does not affect id stable-ness
+ // expect(await getOutput(() => factory())).toBe(expected)
+ // expect(await getOutput(() => factory())).toBe(expected)
+ // })
+ // test('async setup()', async () => {
+ // const factory = (
+ // delay1: number,
+ // delay2: number,
+ // ): ReturnType<TestCaseFactory> => {
+ // const p1 = promiseWithDelay(null, delay1)
+ // const p2 = promiseWithDelay(null, delay2)
+ // const ASOne = defineComponent({
+ // async setup() {
+ // await p1
+ // return {}
+ // },
+ // render() {
+ // return h(BasicComponentWithUseId)
+ // },
+ // })
+ // const ASTwo = defineComponent({
+ // async setup() {
+ // await p2
+ // return {}
+ // },
+ // render() {
+ // return h(BasicComponentWithUseId)
+ // },
+ // })
+ // const app = createApp({
+ // setup() {
+ // const id1 = useId()
+ // const id2 = useId()
+ // return () =>
+ // h(Suspense, null, {
+ // default: h('div', [id1, ' ', id2, ' ', h(ASOne), ' ', h(ASTwo)]),
+ // })
+ // },
+ // })
+ // return [app, [p1, p2]]
+ // }
+ // const expected =
+ // '<div>' +
+ // 'v-0 v-1 ' + // root
+ // 'v-0-0 v-0-1 ' + // inside first async subtree
+ // 'v-1-0 v-1-1' + // inside second async subtree
+ // '</div>'
+ // // assert different async resolution order does not affect id stable-ness
+ // expect(await getOutput(() => factory(0, 16))).toBe(expected)
+ // expect(await getOutput(() => factory(16, 0))).toBe(expected)
+ // })
+ // test('deep nested', async () => {
+ // const factory = (): ReturnType<TestCaseFactory> => {
+ // const p = Promise.resolve()
+ // const One = {
+ // async setup() {
+ // const id = useId()
+ // await p
+ // return () => [id, ' ', h(Two), ' ', h(Three)]
+ // },
+ // }
+ // const Two = {
+ // async setup() {
+ // const id = useId()
+ // await p
+ // return () => [id, ' ', h(Three), ' ', h(Three)]
+ // },
+ // }
+ // const Three = {
+ // async setup() {
+ // const id = useId()
+ // return () => id
+ // },
+ // }
+ // const app = createApp({
+ // setup() {
+ // return () =>
+ // h(Suspense, null, {
+ // default: h(One),
+ // })
+ // },
+ // })
+ // return [app, [p]]
+ // }
+ // const expected =
+ // 'v-0 ' + // One
+ // 'v-0-0 ' + // Two
+ // 'v-0-0-0 v-0-0-1 ' + // Three + Three nested in Two
+ // 'v-0-1' // Three after Two
+ // // assert different async resolution order does not affect id stable-ness
+ // expect(await getOutput(() => factory())).toBe(expected)
+ // expect(await getOutput(() => factory())).toBe(expected)
+ // })
+ // test('async component inside async setup, already resolved', async () => {
+ // const factory = async (
+ // delay1: number,
+ // delay2: number,
+ // ): Promise<FactoryRes> => {
+ // const p1 = promiseWithDelay(null, delay1)
+ // const p2 = promiseWithDelay(BasicComponentWithUseId, delay2)
+ // const AsyncInner = defineAsyncComponent(() => p2)
+ // const AsyncSetup = defineComponent({
+ // async setup() {
+ // await p1
+ // return {}
+ // },
+ // render() {
+ // return h(AsyncInner)
+ // },
+ // })
+ // const app = createApp({
+ // setup() {
+ // const id1 = useId()
+ // const id2 = useId()
+ // return () =>
+ // h(Suspense, null, {
+ // default: h('div', [id1, ' ', id2, ' ', h(AsyncSetup)]),
+ // })
+ // },
+ // })
+ // // the async component may have already been resolved
+ // await AsyncInner.__asyncLoader()
+ // return [app, [p1, p2]]
+ // }
+ // const expected =
+ // '<div>' +
+ // 'v-0 v-1 ' + // root
+ // 'v-0-0-0 v-0-0-1' + // async component inside async setup
+ // '</div>'
+ // // assert different async resolution order does not affect id stable-ness
+ // expect(await getOutput(async () => factory(0, 16))).toBe(expected)
+ // expect(await getOutput(() => factory(16, 0))).toBe(expected)
+ // })
+})
getFunctionalFallthrough,
isAsyncWrapper,
isKeepAlive,
+ markAsyncBoundary,
nextUid,
popWarningContext,
pushWarningContext,
invokeArrayFns,
isArray,
isFunction,
+ isPromise,
isString,
} from '@vue/shared'
import {
} from './insertionState'
import { DynamicFragment, isFragment } from './fragment'
import type { VaporElement } from './apiDefineVaporCustomElement'
+import { parentSuspense, setParentSuspense } from './components/Suspense'
export { currentInstance } from '@vue/runtime-dom'
const parentInstance = getParentInstance()
+ let prevSuspense: SuspenseBoundary | null = null
+ if (__FEATURE_SUSPENSE__ && parentInstance && parentInstance.suspense) {
+ prevSuspense = setParentSuspense(parentInstance.suspense)
+ }
+
if (
(isSingleRoot ||
// transition has attrs fallthrough
endMeasure(instance, 'init')
}
+ if (__FEATURE_SUSPENSE__ && parentInstance && parentInstance.suspense) {
+ setParentSuspense(prevSuspense)
+ }
+
// restore currentSlotConsumer to previous value after setupFn is called
setCurrentSlotConsumer(prevSlotConsumer)
onScopeDispose(() => unmountComponent(instance), true)
]) || EMPTY_OBJ
: EMPTY_OBJ
- if (__DEV__ && !isBlock(setupResult)) {
- if (isFunction(component)) {
- warn(`Functional vapor component must return a block directly.`)
- instance.block = []
- } else if (!component.render) {
+ const isAsyncSetup = isPromise(setupResult)
+
+ if ((isAsyncSetup || instance.sp) && !isAsyncWrapper(instance)) {
+ // async setup / serverPrefetch, mark as async boundary for useId()
+ markAsyncBoundary(instance)
+ }
+
+ if (isAsyncSetup) {
+ if (__FEATURE_SUSPENSE__) {
+ // async setup returned Promise.
+ // bail here and wait for re-entry.
+ instance.asyncDep = setupResult
+ if (__DEV__ && !instance.suspense) {
+ const name = getComponentName(component) ?? 'Anonymous'
+ warn(
+ `Component <${name}>: setup function returned a promise, but no ` +
+ `<Suspense> boundary was found in the parent component tree. ` +
+ `A component with async setup() must be nested in a <Suspense> ` +
+ `in order to be rendered.`,
+ )
+ }
+ } else if (__DEV__) {
warn(
- `Vapor component setup() returned non-block value, and has no render function.`,
+ `setup() returned a Promise, but the version of Vue you are using ` +
+ `does not support it yet.`,
)
- instance.block = []
- } else {
- if (__DEV__ || __FEATURE_PROD_DEVTOOLS__) {
- instance.devtoolsRawSetupState = setupResult
- }
- instance.setupState = proxyRefs(setupResult)
- if (__DEV__) {
- instance.setupState = createDevSetupStateProxy(instance)
- }
- devRender(instance)
}
} else {
- // component has a render function but no setup function
- // (typically components with only a template and no state)
- if (!setupFn && component.render) {
- instance.block = callWithErrorHandling(
- component.render,
- instance,
- ErrorCodes.RENDER_FUNCTION,
- )
- } else {
- // in prod result can only be block
- instance.block = setupResult as Block
- }
- }
-
- // single root, inherit attrs
- if (
- instance.hasFallthrough &&
- component.inheritAttrs !== false &&
- Object.keys(instance.attrs).length
- ) {
- const root = getRootElement(
- instance.block,
- // attach attrs to root dynamic fragments for applying during each update
- frag => (frag.attrs = instance.attrs),
- false,
- )
- if (root) {
- renderEffect(() => {
- const attrs =
- isFunction(component) && !isVaporTransition(component)
- ? getFunctionalFallthrough(instance.attrs)
- : instance.attrs
- if (attrs) applyFallthroughProps(root, attrs)
- })
- } else if (
- __DEV__ &&
- ((!instance.accessedAttrs &&
- isArray(instance.block) &&
- instance.block.length) ||
- // preventing attrs fallthrough on Teleport
- // consistent with VDOM Teleport behavior
- instance.block instanceof TeleportFragment)
- ) {
- warnExtraneousAttributes(instance.attrs)
- }
+ handleSetupResult(setupResult, component, instance, setupFn)
}
setActiveSub(prevSub)
ids: [string, number, number]
// for suspense
suspense: SuspenseBoundary | null
+ suspenseId: number
+ asyncDep: Promise<any> | null
+ asyncResolved: boolean
// for HMR and vapor custom element
// all render effects associated with this instance
this.emit = emit.bind(null, this)
this.expose = expose.bind(null, this)
this.refs = EMPTY_OBJ
- this.emitted =
- this.exposed =
- this.exposeProxy =
- this.propsDefaults =
- this.suspense =
- null
+ this.emitted = this.exposed = this.exposeProxy = this.propsDefaults = null
+
+ // suspense related
+ this.suspense = parentSuspense
+ this.suspenseId = parentSuspense ? parentSuspense.pendingId : 0
+ this.asyncDep = null
+ this.asyncResolved = false
this.isMounted =
this.isUnmounted =
parent: ParentNode,
anchor?: Node | null | 0,
): void {
+ if (
+ __FEATURE_SUSPENSE__ &&
+ instance.suspense &&
+ instance.asyncDep &&
+ !instance.asyncResolved
+ ) {
+ const component = instance.type
+ instance.suspense.registerDep(instance, setupResult => {
+ handleSetupResult(
+ setupResult,
+ component,
+ instance,
+ isFunction(component) ? component : component.setup,
+ )
+ mountComponent(instance, parent, anchor)
+ })
+ return
+ }
+
if (instance.shapeFlag! & ShapeFlags.COMPONENT_KEPT_ALIVE) {
findParentKeepAlive(instance)!.activate(instance, parent, anchor)
return
function isVaporTransition(component: VaporComponent): boolean {
return getComponentName(component) === 'VaporTransition'
}
+
+function handleSetupResult(
+ setupResult: any,
+ component: VaporComponent,
+ instance: VaporComponentInstance,
+ setupFn: VaporSetupFn | undefined,
+) {
+ if (__DEV__) {
+ pushWarningContext(instance)
+ }
+
+ if (__DEV__ && !isBlock(setupResult)) {
+ if (isFunction(component)) {
+ warn(`Functional vapor component must return a block directly.`)
+ instance.block = []
+ } else if (!component.render) {
+ warn(
+ `Vapor component setup() returned non-block value, and has no render function.`,
+ )
+ instance.block = []
+ } else {
+ if (__DEV__ || __FEATURE_PROD_DEVTOOLS__) {
+ instance.devtoolsRawSetupState = setupResult
+ }
+ instance.setupState = proxyRefs(setupResult)
+ if (__DEV__) {
+ instance.setupState = createDevSetupStateProxy(instance)
+ }
+ devRender(instance)
+ }
+ } else {
+ // component has a render function but no setup function
+ // (typically components with only a template and no state)
+ if (!setupFn && component.render) {
+ instance.block = callWithErrorHandling(
+ component.render,
+ instance,
+ ErrorCodes.RENDER_FUNCTION,
+ )
+ } else {
+ // in prod result can only be block
+ instance.block = setupResult as Block
+ }
+ }
+
+ // single root, inherit attrs
+ if (
+ instance.hasFallthrough &&
+ component.inheritAttrs !== false &&
+ Object.keys(instance.attrs).length
+ ) {
+ const root = getRootElement(
+ instance.block,
+ // attach attrs to root dynamic fragments for applying during each update
+ frag => (frag.attrs = instance.attrs),
+ false,
+ )
+ if (root) {
+ renderEffect(() => {
+ const attrs =
+ isFunction(component) && !isVaporTransition(component)
+ ? getFunctionalFallthrough(instance.attrs)
+ : instance.attrs
+ if (attrs) applyFallthroughProps(root, attrs)
+ })
+ } else if (
+ __DEV__ &&
+ ((!instance.accessedAttrs &&
+ isArray(instance.block) &&
+ instance.block.length) ||
+ // preventing attrs fallthrough on Teleport
+ // consistent with VDOM Teleport behavior
+ instance.block instanceof TeleportFragment)
+ ) {
+ warnExtraneousAttributes(instance.attrs)
+ }
+ }
+
+ if (__DEV__) {
+ popWarningContext()
+ }
+}
--- /dev/null
+import type { SuspenseBoundary } from '@vue/runtime-dom'
+
+export let parentSuspense: SuspenseBoundary | null = null
+
+export function setParentSuspense(
+ suspense: SuspenseBoundary | null,
+): SuspenseBoundary | null {
+ try {
+ return parentSuspense
+ } finally {
+ parentSuspense = suspense
+ }
+}
+
+// TODO: implement this
+export const VaporSuspenseImpl = {
+ name: 'VaporSuspense',
+ __isSuspense: true,
+ process(): void {},
+}
type RendererNode,
type ShallowRef,
type Slots,
+ type SuspenseBoundary,
type TransitionHooks,
type VNode,
type VNodeNormalizedRef,
deactivate,
findParentKeepAlive,
} from './components/KeepAlive'
+import { setParentSuspense } from './components/Suspense'
export const interopKey: unique symbol = Symbol(`interop`)
VaporInteropInterface,
'vdomMount' | 'vdomUnmount' | 'vdomSlot'
> = {
- mount(vnode, container, anchor, parentComponent) {
+ mount(vnode, container, anchor, parentComponent, parentSuspense) {
let selfAnchor = (vnode.el = vnode.anchor = createTextNode())
if (isHydrating) {
// avoid vdom hydration children mismatch by the selfAnchor, delay its insertion
const propsRef = shallowRef(props)
const slotsRef = shallowRef(vnode.children)
+ let prevSuspense: SuspenseBoundary | null = null
+ if (__FEATURE_SUSPENSE__ && parentSuspense) {
+ prevSuspense = setParentSuspense(parentSuspense)
+ }
+
const dynamicPropSource: (() => any)[] & { [interopKey]?: boolean } = [
() => propsRef.value,
]
vnode.transition as VaporTransitionHooks,
)
}
+
+ if (__FEATURE_SUSPENSE__ && parentSuspense) {
+ setParentSuspense(prevSuspense)
+ }
+
mountComponent(instance, container, selfAnchor)
simpleSetCurrentInstance(prev)
return instance
unmount(vnode, doRemove) {
const container = doRemove ? vnode.anchor!.parentNode : undefined
- if (vnode.component) {
- unmountComponent(vnode.component as any, container)
+ const instance = vnode.component as any as VaporComponentInstance
+ if (instance) {
+ // the async component may not be resolved yet, block is null
+ if (instance.block) {
+ unmountComponent(instance, container)
+ }
} else if (vnode.vb) {
remove(vnode.vb, container)
}
insert(vnode.anchor as any, container, anchor)
},
- hydrate(vnode, node, container, anchor, parentComponent) {
+ hydrate(vnode, node, container, anchor, parentComponent, parentSuspense) {
vaporHydrateNode(node, () =>
- this.mount(vnode, container, anchor, parentComponent),
+ this.mount(vnode, container, anchor, parentComponent, parentSuspense),
)
return _next(node)
},