fix(suspense): avoid double-patching nested suspense when parent suspense is not resolved (#10055)

close #8678
This commit is contained in:
edison 2024-01-11 17:27:53 +08:00 committed by GitHub
parent 07b19a53a5
commit bcda96b525
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
2 changed files with 147 additions and 0 deletions

View File

@ -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)
/**
* <Suspense>
* <component :is="outerToggle ? outerB : outerA">
* <Suspense>
* <component :is="innerToggle ? innerB : innerA" />
* </Suspense>
* </component>
* </Suspense>
*/
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(`<div>fallback outer</div>`)
// mount outer component
await Promise.all(deps)
await nextTick()
expect(serializeInner(root)).toBe(`<div>outerA</div><!---->`)
expect(calls).toEqual([`outerA created`, `outerA mounted`])
// mount inner component
await Promise.all(deps)
await nextTick()
expect(serializeInner(root)).toBe(`<div>outerA</div><div>innerA</div>`)
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(`<div>outerB</div><!---->`)
await Promise.all(deps)
await nextTick()
expect(serializeInner(root)).toBe(`<div>outerB</div><div>innerB</div>`)
// innerB only mount once
expect(calls).toEqual([
'outerB created',
'outerB mounted',
'innerB created',
'innerB mounted',
])
})
// #6416
test('KeepAlive with Suspense', async () => {
const Async = defineAsyncComponent({

View File

@ -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,