diff --git a/packages/runtime-core/__tests__/components/Suspense.spec.ts b/packages/runtime-core/__tests__/components/Suspense.spec.ts index 80d213fc790..2942624dd10 100644 --- a/packages/runtime-core/__tests__/components/Suspense.spec.ts +++ b/packages/runtime-core/__tests__/components/Suspense.spec.ts @@ -19,7 +19,7 @@ import { shallowRef, Fragment } from '@vue/runtime-test' -import { createApp } from 'vue' +import { createApp, defineComponent } from 'vue' describe('Suspense', () => { const deps: Promise[] = [] @@ -1335,9 +1335,14 @@ describe('Suspense', () => { h(Suspense, null, { default: [ h(outerToggle.value ? OuterB : OuterA, null, { - default: () => h(Suspense, { suspensible: true },{ - default: h(innerToggle.value ? InnerB : InnerA) - }) + default: () => + h( + Suspense, + { suspensible: true }, + { + default: h(innerToggle.value ? InnerB : InnerA) + } + ) }) ], fallback: h('div', 'fallback outer') @@ -1400,4 +1405,122 @@ describe('Suspense', () => { expect(serializeInner(root)).toBe(expected) expect(calls).toContain('innerB mounted') }) + + // #8206 + test('nested suspense with suspensible & no async deps', async () => { + const calls: string[] = [] + let expected = '' + + const InnerA = defineComponent({ + setup: () => { + calls.push('innerA created') + onMounted(() => { + calls.push('innerA mounted') + }) + return () => h('div', 'innerA') + } + }) + + const InnerB = defineComponent({ + setup: () => { + calls.push('innerB created') + onMounted(() => { + calls.push('innerB mounted') + }) + return () => h('div', 'innerB') + } + }) + + const OuterA = defineComponent({ + setup: (_, { slots }: any) => { + calls.push('outerA created') + onMounted(() => { + calls.push('outerA mounted') + }) + return () => h(Fragment, null, [h('div', 'outerA'), slots.default?.()]) + } + }) + + const OuterB = defineComponent({ + setup: (_, { slots }: any) => { + calls.push('outerB created') + onMounted(() => { + calls.push('outerB mounted') + }) + return () => h(Fragment, null, [h('div', 'outerB'), slots.default?.()]) + } + }) + + const outerToggle = ref(false) + const innerToggle = ref(false) + + /** + * + * + * + * + * + * + * + */ + const Comp = defineComponent({ + setup() { + return () => + h(Suspense, null, { + default: [ + h(outerToggle.value ? OuterB : OuterA, null, { + default: () => + h( + Suspense, + { suspensible: true }, + { + default: h(innerToggle.value ? InnerB : InnerA) + } + ) + }) + ], + fallback: h('div', 'fallback outer') + }) + } + }) + + expected = `
outerA
innerA
` + const root = nodeOps.createElement('div') + render(h(Comp), root) + expect(serializeInner(root)).toBe(expected) + + // mount outer component and inner component + await Promise.all(deps) + await nextTick() + + expect(serializeInner(root)).toBe(expected) + expect(calls).toEqual([ + 'outerA created', + 'innerA created', + 'innerA mounted', + 'outerA mounted' + ]) + + // toggle outer component + calls.length = 0 + deps.length = 0 + outerToggle.value = true + await nextTick() + + await Promise.all(deps) + await nextTick() + expected = `
outerB
innerA
` + expect(serializeInner(root)).toBe(expected) + expect(calls).toContain('outerB mounted') + expect(calls).toContain('innerA mounted') + + // toggle inner component + calls.length = 0 + deps.length = 0 + innerToggle.value = true + await Promise.all(deps) + await nextTick() + expected = `
outerB
innerB
` + expect(serializeInner(root)).toBe(expected) + }) }) diff --git a/packages/runtime-core/src/components/Suspense.ts b/packages/runtime-core/src/components/Suspense.ts index fd6a7e0fac8..174f33e13ad 100644 --- a/packages/runtime-core/src/components/Suspense.ts +++ b/packages/runtime-core/src/components/Suspense.ts @@ -184,7 +184,7 @@ function mountSuspense( setActiveBranch(suspense, vnode.ssFallback!) } else { // Suspense has no async deps. Just resolve. - suspense.resolve() + suspense.resolve(false, true) } } @@ -388,7 +388,7 @@ export interface SuspenseBoundary { isHydrating: boolean isUnmounted: boolean effects: Function[] - resolve(force?: boolean): void + resolve(force?: boolean, sync?: boolean): void fallback(fallbackVNode: VNode): void move( container: RendererElement, @@ -437,11 +437,10 @@ function createSuspenseBoundary( // if set `suspensible: true`, set the current suspense as a dep of parent suspense let parentSuspenseId: number | undefined - const isSuspensible = - vnode.props?.suspensible != null && vnode.props.suspensible !== false + const isSuspensible = isVNodeSuspensible(vnode) if (isSuspensible) { if (parentSuspense?.pendingBranch) { - parentSuspenseId = parentSuspense?.pendingId + parentSuspenseId = parentSuspense.pendingId parentSuspense.deps++ } } @@ -469,7 +468,7 @@ function createSuspenseBoundary( isUnmounted: false, effects: [], - resolve(resume = false) { + resolve(resume = false, sync = false) { if (__DEV__) { if (!resume && !suspense.pendingBranch) { throw new Error( @@ -553,7 +552,7 @@ function createSuspenseBoundary( parentSuspenseId === parentSuspense.pendingId ) { parentSuspense.deps-- - if (parentSuspense.deps === 0) { + if (parentSuspense.deps === 0 && !sync) { parentSuspense.resolve() } } @@ -831,3 +830,7 @@ function setActiveBranch(suspense: SuspenseBoundary, branch: VNode) { updateHOCHostEl(parentComponent, el) } } + +function isVNodeSuspensible(vnode: VNode) { + return vnode.props?.suspensible != null && vnode.props.suspensible !== false +}