mirror of https://github.com/vuejs/core.git
fix(suspense): avoid double-patching nested suspense when parent suspense is not resolved (#10055)
close #8678
This commit is contained in:
parent
07b19a53a5
commit
bcda96b525
|
@ -1641,6 +1641,141 @@ describe('Suspense', () => {
|
||||||
expect(serializeInner(root)).toBe(expected)
|
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
|
// #6416
|
||||||
test('KeepAlive with Suspense', async () => {
|
test('KeepAlive with Suspense', async () => {
|
||||||
const Async = defineAsyncComponent({
|
const Async = defineAsyncComponent({
|
||||||
|
|
|
@ -91,6 +91,18 @@ export const SuspenseImpl = {
|
||||||
rendererInternals,
|
rendererInternals,
|
||||||
)
|
)
|
||||||
} else {
|
} 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(
|
patchSuspense(
|
||||||
n1,
|
n1,
|
||||||
n2,
|
n2,
|
||||||
|
|
Loading…
Reference in New Issue