From 5966fec206b689b970a937e89cbbd34a12ed013b Mon Sep 17 00:00:00 2001 From: daiwei Date: Tue, 10 Jun 2025 11:26:36 +0800 Subject: [PATCH 1/2] fix(suspense): don't immediately resolve suspense on last dep unmount because new vnodes may contain async components --- packages/runtime-core/src/renderer.ts | 18 ------------------ 1 file changed, 18 deletions(-) diff --git a/packages/runtime-core/src/renderer.ts b/packages/runtime-core/src/renderer.ts index a57be791a44..16ed847076a 100644 --- a/packages/runtime-core/src/renderer.ts +++ b/packages/runtime-core/src/renderer.ts @@ -2332,24 +2332,6 @@ function baseCreateRenderer( instance.isUnmounted = true }, parentSuspense) - // A component with async dep inside a pending suspense is unmounted before - // its async dep resolves. This should remove the dep from the suspense, and - // cause the suspense to resolve immediately if that was the last dep. - if ( - __FEATURE_SUSPENSE__ && - parentSuspense && - parentSuspense.pendingBranch && - !parentSuspense.isUnmounted && - instance.asyncDep && - !instance.asyncResolved && - instance.suspenseId === parentSuspense.pendingId - ) { - parentSuspense.deps-- - if (parentSuspense.deps === 0) { - parentSuspense.resolve() - } - } - if (__DEV__ || __FEATURE_PROD_DEVTOOLS__) { devtoolsComponentRemoved(instance) } From d88b2f4983d0f94cc077f01ba7779bf7945760cf Mon Sep 17 00:00:00 2001 From: daiwei Date: Tue, 10 Jun 2025 14:11:59 +0800 Subject: [PATCH 2/2] test: add tests --- .../__tests__/components/Suspense.spec.ts | 76 +++++++++++++++++++ 1 file changed, 76 insertions(+) diff --git a/packages/runtime-core/__tests__/components/Suspense.spec.ts b/packages/runtime-core/__tests__/components/Suspense.spec.ts index 65e801de277..2bcb5f744fd 100644 --- a/packages/runtime-core/__tests__/components/Suspense.spec.ts +++ b/packages/runtime-core/__tests__/components/Suspense.spec.ts @@ -17,6 +17,8 @@ import { onUnmounted, ref, render, + renderList, + renderSlot, resolveDynamicComponent, serializeInner, shallowRef, @@ -2161,6 +2163,80 @@ describe('Suspense', () => { await Promise.all(deps) }) + // #13453 + test('add new async deps during patching', async () => { + const getComponent = (type: string) => { + if (type === 'A') { + return defineAsyncComponent({ + setup() { + return () => h('div', 'A') + }, + }) + } + return defineAsyncComponent({ + setup() { + return () => h('div', 'B') + }, + }) + } + + const types = ref(['A']) + const add = async () => { + types.value.push('B') + } + + const update = async () => { + // mount Suspense B + // [Suspense A] -> [Suspense A(pending), Suspense B(pending)] + await add() + // patch Suspense B (still pending) + // [Suspense A(pending), Suspense B(pending)] -> [Suspense B(pending)] + types.value.shift() + } + + const Comp = { + render(this: any) { + return h(Fragment, null, [ + renderList(types.value, type => { + return h( + Suspense, + { key: type }, + { + default: () => [ + renderSlot(this.$slots, 'default', { type: type }), + ], + }, + ) + }), + ]) + }, + } + + const App = { + setup() { + return () => + h(Comp, null, { + default: (params: any) => [h(getComponent(params.type))], + }) + }, + } + + const root = nodeOps.createElement('div') + render(h(App), root) + expect(serializeInner(root)).toBe(``) + + await Promise.all(deps) + expect(serializeInner(root)).toBe(`
A
`) + + update() + await nextTick() + // wait for both A and B to resolve + await Promise.all(deps) + // wait for new B to resolve + await Promise.all(deps) + expect(serializeInner(root)).toBe(`
B
`) + }) + describe('warnings', () => { // base function to check if a combination of slots warns or not function baseCheckWarn(