diff --git a/packages/runtime-core/__tests__/rendererSuspense.spec.ts b/packages/runtime-core/__tests__/rendererSuspense.spec.ts
index 5e80267b9..fa5b6716a 100644
--- a/packages/runtime-core/__tests__/rendererSuspense.spec.ts
+++ b/packages/runtime-core/__tests__/rendererSuspense.spec.ts
@@ -105,6 +105,53 @@ describe('renderer: suspense', () => {
expect(serializeInner(root)).toBe(`
async
`)
})
+ test('nested async deps', async () => {
+ const calls: string[] = []
+
+ const AsyncOuter = createAsyncComponent({
+ setup() {
+ onMounted(() => {
+ calls.push('outer mounted')
+ })
+ return () => h(AsyncInner)
+ }
+ })
+
+ const AsyncInner = createAsyncComponent(
+ {
+ setup() {
+ onMounted(() => {
+ calls.push('inner mounted')
+ })
+ return () => h('div', 'inner')
+ }
+ },
+ 10
+ )
+
+ const Comp = {
+ setup() {
+ return () =>
+ h(Suspense, null, {
+ default: h(AsyncOuter),
+ fallback: h('div', 'fallback')
+ })
+ }
+ }
+
+ const root = nodeOps.createElement('div')
+ render(h(Comp), root)
+ expect(serializeInner(root)).toBe(`fallback
`)
+
+ await deps[0]
+ await nextTick()
+ expect(serializeInner(root)).toBe(`fallback
`)
+
+ await Promise.all(deps)
+ await nextTick()
+ expect(serializeInner(root)).toBe(`inner
`)
+ })
+
test('onResolve', async () => {
const Async = createAsyncComponent({
render() {
@@ -286,15 +333,219 @@ describe('renderer: suspense', () => {
expect(calls).toEqual([])
})
- test('unmount suspense after resolve', () => {})
+ test('unmount suspense after resolve', async () => {
+ const toggle = ref(true)
+ const unmounted = jest.fn()
- test.todo('unmount suspense before resolve')
+ const Async = createAsyncComponent({
+ setup() {
+ onUnmounted(unmounted)
+ return () => h('div', 'async')
+ }
+ })
- test.todo('nested suspense')
+ const Comp = {
+ setup() {
+ return () =>
+ toggle.value
+ ? h(Suspense, null, {
+ default: h(Async),
+ fallback: h('div', 'fallback')
+ })
+ : null
+ }
+ }
- test.todo('new async dep after resolve should cause suspense to restart')
+ const root = nodeOps.createElement('div')
+ render(h(Comp), root)
+ expect(serializeInner(root)).toBe(`fallback
`)
+
+ await Promise.all(deps)
+ await nextTick()
+ expect(serializeInner(root)).toBe(`async
`)
+ expect(unmounted).not.toHaveBeenCalled()
+
+ toggle.value = false
+ await nextTick()
+ expect(serializeInner(root)).toBe(``)
+ expect(unmounted).toHaveBeenCalled()
+ })
+
+ test('unmount suspense before resolve', async () => {
+ const toggle = ref(true)
+ const mounted = jest.fn()
+ const unmounted = jest.fn()
+
+ const Async = createAsyncComponent({
+ setup() {
+ onMounted(mounted)
+ onUnmounted(unmounted)
+ return () => h('div', 'async')
+ }
+ })
+
+ const Comp = {
+ setup() {
+ return () =>
+ toggle.value
+ ? h(Suspense, null, {
+ default: h(Async),
+ fallback: h('div', 'fallback')
+ })
+ : null
+ }
+ }
+
+ const root = nodeOps.createElement('div')
+ render(h(Comp), root)
+ expect(serializeInner(root)).toBe(`fallback
`)
+
+ toggle.value = false
+ await nextTick()
+ expect(serializeInner(root)).toBe(``)
+ expect(mounted).not.toHaveBeenCalled()
+ expect(unmounted).not.toHaveBeenCalled()
+
+ await Promise.all(deps)
+ await nextTick()
+ // should not resolve and cause unmount
+ expect(mounted).not.toHaveBeenCalled()
+ expect(unmounted).not.toHaveBeenCalled()
+ })
+
+ test('nested suspense (parent resolves first)', async () => {
+ const calls: string[] = []
+
+ const AsyncOuter = createAsyncComponent(
+ {
+ setup: () => {
+ onMounted(() => {
+ calls.push('outer mounted')
+ })
+ return () => h('div', 'async outer')
+ }
+ },
+ 1
+ )
+
+ const AsyncInner = createAsyncComponent(
+ {
+ setup: () => {
+ onMounted(() => {
+ calls.push('inner mounted')
+ })
+ return () => h('div', 'async inner')
+ }
+ },
+ 10
+ )
+
+ const Inner = {
+ setup() {
+ return () =>
+ h(Suspense, null, {
+ default: h(AsyncInner),
+ fallback: h('div', 'fallback inner')
+ })
+ }
+ }
+
+ const Comp = {
+ setup() {
+ return () =>
+ h(Suspense, null, {
+ default: [h(AsyncOuter), h(Inner)],
+ fallback: h('div', 'fallback outer')
+ })
+ }
+ }
+
+ const root = nodeOps.createElement('div')
+ render(h(Comp), root)
+ expect(serializeInner(root)).toBe(`fallback outer
`)
+
+ await deps[0]
+ await nextTick()
+ expect(serializeInner(root)).toBe(
+ `async outer
fallback inner
`
+ )
+ expect(calls).toEqual([`outer mounted`])
+
+ await Promise.all(deps)
+ await nextTick()
+ expect(serializeInner(root)).toBe(
+ `async outer
async inner
`
+ )
+ expect(calls).toEqual([`outer mounted`, `inner mounted`])
+ })
+
+ test('nested suspense (child resolves first)', async () => {
+ const calls: string[] = []
+
+ const AsyncOuter = createAsyncComponent(
+ {
+ setup: () => {
+ onMounted(() => {
+ calls.push('outer mounted')
+ })
+ return () => h('div', 'async outer')
+ }
+ },
+ 10
+ )
+
+ const AsyncInner = createAsyncComponent(
+ {
+ setup: () => {
+ onMounted(() => {
+ calls.push('inner mounted')
+ })
+ return () => h('div', 'async inner')
+ }
+ },
+ 1
+ )
+
+ const Inner = {
+ setup() {
+ return () =>
+ h(Suspense, null, {
+ default: h(AsyncInner),
+ fallback: h('div', 'fallback inner')
+ })
+ }
+ }
+
+ const Comp = {
+ setup() {
+ return () =>
+ h(Suspense, null, {
+ default: [h(AsyncOuter), h(Inner)],
+ fallback: h('div', 'fallback outer')
+ })
+ }
+ }
+
+ const root = nodeOps.createElement('div')
+ render(h(Comp), root)
+ expect(serializeInner(root)).toBe(`fallback outer
`)
+
+ await deps[1]
+ await nextTick()
+ expect(serializeInner(root)).toBe(`fallback outer
`)
+ expect(calls).toEqual([])
+
+ await Promise.all(deps)
+ await nextTick()
+ expect(serializeInner(root)).toBe(
+ `async outer
async inner
`
+ )
+ expect(calls).toEqual([`inner mounted`, `outer mounted`])
+ })
test.todo('error handling')
+ test.todo('new async dep after resolve should cause suspense to restart')
+
test.todo('portal inside suspense')
})
diff --git a/packages/runtime-core/src/component.ts b/packages/runtime-core/src/component.ts
index 500ac828a..5802549c4 100644
--- a/packages/runtime-core/src/component.ts
+++ b/packages/runtime-core/src/component.ts
@@ -1,4 +1,4 @@
-import { VNode, VNodeChild } from './vnode'
+import { VNode, VNodeChild, isVNode } from './vnode'
import { ReactiveEffect, reactive, readonly } from '@vue/reactivity'
import {
PublicInstanceProxyHandlers,
@@ -279,6 +279,12 @@ export function handleSetupResult(
// setup returned an inline render function
instance.render = setupResult as RenderFunction
} else if (isObject(setupResult)) {
+ if (__DEV__ && isVNode(setupResult)) {
+ warn(
+ `setup() should not return VNodes directly - ` +
+ `return a render function instead.`
+ )
+ }
// setup returned bindings.
// assuming a render function compiled from template is present.
instance.renderContext = reactive(setupResult)
diff --git a/packages/runtime-core/src/createRenderer.ts b/packages/runtime-core/src/createRenderer.ts
index 9d665f481..020f7c8fc 100644
--- a/packages/runtime-core/src/createRenderer.ts
+++ b/packages/runtime-core/src/createRenderer.ts
@@ -871,6 +871,7 @@ export function createRenderer<
hasUnresolvedAncestor = true
break
}
+ parent = parent.parent
}
// no pending parent suspense, flush all jobs
if (!hasUnresolvedAncestor) {
@@ -1509,7 +1510,14 @@ export function createRenderer<
return
}
if (__FEATURE_SUSPENSE__ && vnode.type === Suspense) {
- move((vnode.suspense as any).subTree, container, anchor)
+ const suspense = vnode.suspense as SuspenseBoundary
+ move(
+ suspense.isResolved ? suspense.subTree : suspense.fallbackTree,
+ container,
+ anchor
+ )
+ suspense.container = container
+ // suspense.anchor = anchor
return
}
if (vnode.type === Fragment) {
diff --git a/packages/runtime-core/src/vnode.ts b/packages/runtime-core/src/vnode.ts
index 8bcafdd7f..589042ec3 100644
--- a/packages/runtime-core/src/vnode.ts
+++ b/packages/runtime-core/src/vnode.ts
@@ -124,6 +124,12 @@ export function createBlock(
return vnode
}
+const knownVNodes = new WeakSet()
+
+export function isVNode(value: any): boolean {
+ return knownVNodes.has(value)
+}
+
export function createVNode(
type: VNodeTypes,
props: { [key: string]: any } | null | 0 = null,
@@ -198,6 +204,10 @@ export function createVNode(
trackDynamicNode(vnode)
}
+ if (__DEV__) {
+ knownVNodes.add(vnode)
+ }
+
return vnode
}