mirror of https://github.com/vuejs/core.git
feat(suspense): introduce suspensible option for `<Suspense>` (#6736)
close #5513
This commit is contained in:
parent
15847f38a0
commit
cb37d0b9ff
|
@ -16,7 +16,8 @@ import {
|
|||
watchEffect,
|
||||
onUnmounted,
|
||||
onErrorCaptured,
|
||||
shallowRef
|
||||
shallowRef,
|
||||
Fragment
|
||||
} from '@vue/runtime-test'
|
||||
import { createApp } from 'vue'
|
||||
|
||||
|
@ -1257,4 +1258,146 @@ describe('Suspense', () => {
|
|||
`A component with async setup() must be nested in a <Suspense>`
|
||||
).toHaveBeenWarned()
|
||||
})
|
||||
|
||||
test('nested suspense with suspensible', async () => {
|
||||
const calls: string[] = []
|
||||
let expected = ''
|
||||
|
||||
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 suspensible>
|
||||
* <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, { suspensible: true },{
|
||||
default: h(innerToggle.value ? InnerB : InnerA)
|
||||
})
|
||||
})
|
||||
],
|
||||
fallback: h('div', 'fallback outer')
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
expected = `<div>fallback outer</div>`
|
||||
const root = nodeOps.createElement('div')
|
||||
render(h(Comp), root)
|
||||
expect(serializeInner(root)).toBe(expected)
|
||||
|
||||
// mount outer component
|
||||
await Promise.all(deps)
|
||||
await nextTick()
|
||||
|
||||
expect(serializeInner(root)).toBe(expected)
|
||||
expect(calls).toEqual([`outerA created`])
|
||||
|
||||
// mount inner component
|
||||
await Promise.all(deps)
|
||||
await nextTick()
|
||||
expected = `<div>outerA</div><div>innerA</div>`
|
||||
expect(serializeInner(root)).toBe(expected)
|
||||
|
||||
expect(calls).toEqual([
|
||||
'outerA created',
|
||||
'innerA created',
|
||||
'outerA mounted',
|
||||
'innerA mounted'
|
||||
])
|
||||
|
||||
// toggle outer component
|
||||
calls.length = 0
|
||||
deps.length = 0
|
||||
outerToggle.value = true
|
||||
await nextTick()
|
||||
|
||||
await Promise.all(deps)
|
||||
await nextTick()
|
||||
expect(serializeInner(root)).toBe(expected) // expect not change
|
||||
|
||||
await Promise.all(deps)
|
||||
await nextTick()
|
||||
expected = `<div>outerB</div><div>innerA</div>`
|
||||
expect(serializeInner(root)).toBe(expected)
|
||||
expect(calls).toContain('outerB mounted')
|
||||
expect(calls).toContain('innerA mounted')
|
||||
|
||||
// toggle inner component
|
||||
calls.length = 0
|
||||
deps.length = 0
|
||||
innerToggle.value = true
|
||||
await nextTick()
|
||||
expect(serializeInner(root)).toBe(expected) // expect not change
|
||||
|
||||
await Promise.all(deps)
|
||||
await nextTick()
|
||||
expected = `<div>outerB</div><div>innerB</div>`
|
||||
expect(serializeInner(root)).toBe(expected)
|
||||
expect(calls).toContain('innerB mounted')
|
||||
})
|
||||
})
|
||||
|
|
|
@ -35,6 +35,12 @@ export interface SuspenseProps {
|
|||
onPending?: () => void
|
||||
onFallback?: () => void
|
||||
timeout?: string | number
|
||||
/**
|
||||
* Allow suspense to be captured by parent suspense
|
||||
*
|
||||
* @default false
|
||||
*/
|
||||
suspensible?: boolean
|
||||
}
|
||||
|
||||
export const isSuspense = (type: any): boolean => type.__isSuspense
|
||||
|
@ -395,7 +401,7 @@ let hasWarned = false
|
|||
|
||||
function createSuspenseBoundary(
|
||||
vnode: VNode,
|
||||
parent: SuspenseBoundary | null,
|
||||
parentSuspense: SuspenseBoundary | null,
|
||||
parentComponent: ComponentInternalInstance | null,
|
||||
container: RendererElement,
|
||||
hiddenContainer: RendererElement,
|
||||
|
@ -423,6 +429,17 @@ function createSuspenseBoundary(
|
|||
o: { parentNode, remove }
|
||||
} = rendererInternals
|
||||
|
||||
// if set `suspensible: true`, set the current suspense as a dep of parent suspense
|
||||
let parentSuspenseId: number | undefined
|
||||
const isSuspensible =
|
||||
vnode.props?.suspensible != null && vnode.props.suspensible !== false
|
||||
if (isSuspensible) {
|
||||
if (parentSuspense?.pendingBranch) {
|
||||
parentSuspenseId = parentSuspense?.pendingId
|
||||
parentSuspense.deps++
|
||||
}
|
||||
}
|
||||
|
||||
const timeout = vnode.props ? toNumber(vnode.props.timeout) : undefined
|
||||
if (__DEV__) {
|
||||
assertNumber(timeout, `Suspense timeout`)
|
||||
|
@ -430,7 +447,7 @@ function createSuspenseBoundary(
|
|||
|
||||
const suspense: SuspenseBoundary = {
|
||||
vnode,
|
||||
parent,
|
||||
parent: parentSuspense,
|
||||
parentComponent,
|
||||
isSVG,
|
||||
container,
|
||||
|
@ -522,6 +539,20 @@ function createSuspenseBoundary(
|
|||
}
|
||||
suspense.effects = []
|
||||
|
||||
// resolve parent suspense if all async deps are resolved
|
||||
if (isSuspensible) {
|
||||
if (
|
||||
parentSuspense &&
|
||||
parentSuspense.pendingBranch &&
|
||||
parentSuspenseId === parentSuspense.pendingId
|
||||
) {
|
||||
parentSuspense.deps--
|
||||
if (parentSuspense.deps === 0) {
|
||||
parentSuspense.resolve()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// invoke @resolve event
|
||||
triggerEvent(vnode, 'onResolve')
|
||||
},
|
||||
|
|
Loading…
Reference in New Issue