diff --git a/packages/runtime-core/__tests__/components/Suspense.spec.ts b/packages/runtime-core/__tests__/components/Suspense.spec.ts index 203937aa526..1c0b8e98a49 100644 --- a/packages/runtime-core/__tests__/components/Suspense.spec.ts +++ b/packages/runtime-core/__tests__/components/Suspense.spec.ts @@ -1641,6 +1641,141 @@ describe('Suspense', () => { expect(serializeInner(root)).toBe(expected) }) + //#8678 + test('nested suspense (child suspense update before parent suspense resolve)', async () => { + const calls: string[] = [] + + const InnerA = defineAsyncComponent( + { + setup: () => { + calls.push('innerA created') + onMounted(() => { + calls.push('innerA mounted') + }) + return () => h('div', 'innerA') + }, + }, + 10, + ) + + const InnerB = defineAsyncComponent( + { + setup: () => { + calls.push('innerB created') + onMounted(() => { + calls.push('innerB mounted') + }) + return () => h('div', 'innerB') + }, + }, + 10, + ) + + const OuterA = defineAsyncComponent( + { + setup: (_, { slots }: any) => { + calls.push('outerA created') + onMounted(() => { + calls.push('outerA mounted') + }) + return () => + h(Fragment, null, [h('div', 'outerA'), slots.default?.()]) + }, + }, + 5, + ) + + const OuterB = defineAsyncComponent( + { + setup: (_, { slots }: any) => { + calls.push('outerB created') + onMounted(() => { + calls.push('outerB mounted') + }) + return () => + h(Fragment, null, [h('div', 'outerB'), slots.default?.()]) + }, + }, + 5, + ) + + const outerToggle = ref(false) + const innerToggle = ref(false) + + /** + * + * + * + * + * + * + * + */ + const Comp = { + setup() { + return () => + h(Suspense, null, { + default: [ + h(outerToggle.value ? OuterB : OuterA, null, { + default: () => + h(Suspense, null, { + default: h(innerToggle.value ? InnerB : InnerA), + }), + }), + ], + fallback: h('div', 'fallback outer'), + }) + }, + } + + const root = nodeOps.createElement('div') + render(h(Comp), root) + expect(serializeInner(root)).toBe(`
fallback outer
`) + + // mount outer component + await Promise.all(deps) + await nextTick() + + expect(serializeInner(root)).toBe(`
outerA
`) + expect(calls).toEqual([`outerA created`, `outerA mounted`]) + + // mount inner component + await Promise.all(deps) + await nextTick() + expect(serializeInner(root)).toBe(`
outerA
innerA
`) + + expect(calls).toEqual([ + 'outerA created', + 'outerA mounted', + 'innerA created', + 'innerA mounted', + ]) + + calls.length = 0 + deps.length = 0 + + // toggle both outer and inner components + outerToggle.value = true + innerToggle.value = true + await nextTick() + + await Promise.all(deps) + await nextTick() + expect(serializeInner(root)).toBe(`
outerB
`) + + await Promise.all(deps) + await nextTick() + expect(serializeInner(root)).toBe(`
outerB
innerB
`) + + // innerB only mount once + expect(calls).toEqual([ + 'outerB created', + 'outerB mounted', + 'innerB created', + 'innerB mounted', + ]) + }) + // #6416 test('KeepAlive with Suspense', async () => { const Async = defineAsyncComponent({ diff --git a/packages/runtime-core/src/components/Suspense.ts b/packages/runtime-core/src/components/Suspense.ts index 8d6ee16410a..b9e0ccdd710 100644 --- a/packages/runtime-core/src/components/Suspense.ts +++ b/packages/runtime-core/src/components/Suspense.ts @@ -91,6 +91,18 @@ export const SuspenseImpl = { rendererInternals, ) } else { + // #8678 if the current suspense needs to be patched and parentSuspense has + // not been resolved. this means that both the current suspense and parentSuspense + // need to be patched. because parentSuspense's pendingBranch includes the + // current suspense, it will be processed twice: + // 1. current patch + // 2. mounting along with the pendingBranch of parentSuspense + // it is necessary to skip the current patch to avoid multiple mounts + // of inner components. + if (parentSuspense && parentSuspense.deps > 0) { + n2.suspense = n1.suspense + return + } patchSuspense( n1, n2,