fix(suspense): don't immediately resolve suspense on last dep unmount (#13456)

close #13453
This commit is contained in:
edison 2025-08-20 22:11:16 +08:00 committed by GitHub
parent 0562548ab3
commit a8713159ee
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
2 changed files with 76 additions and 18 deletions

View File

@ -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(`<div>A</div>`)
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(`<div>B</div>`)
})
describe('warnings', () => {
// base function to check if a combination of slots warns or not
function baseCheckWarn(

View File

@ -2326,24 +2326,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)
}