Browse Source

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

close #8678
pull/10036/head
edison 2 years ago
committed by GitHub
parent
commit
bcda96b525
No known key found for this signature in database GPG Key ID: 4AEE18F83AFDEB23
  1. 135
      packages/runtime-core/__tests__/components/Suspense.spec.ts
  2. 12
      packages/runtime-core/src/components/Suspense.ts

135
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)
/**
* <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({

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

Loading…
Cancel
Save