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,
|
watchEffect,
|
||||||
onUnmounted,
|
onUnmounted,
|
||||||
onErrorCaptured,
|
onErrorCaptured,
|
||||||
shallowRef
|
shallowRef,
|
||||||
|
Fragment
|
||||||
} from '@vue/runtime-test'
|
} from '@vue/runtime-test'
|
||||||
import { createApp } from 'vue'
|
import { createApp } from 'vue'
|
||||||
|
|
||||||
|
@ -1257,4 +1258,146 @@ describe('Suspense', () => {
|
||||||
`A component with async setup() must be nested in a <Suspense>`
|
`A component with async setup() must be nested in a <Suspense>`
|
||||||
).toHaveBeenWarned()
|
).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
|
onPending?: () => void
|
||||||
onFallback?: () => void
|
onFallback?: () => void
|
||||||
timeout?: string | number
|
timeout?: string | number
|
||||||
|
/**
|
||||||
|
* Allow suspense to be captured by parent suspense
|
||||||
|
*
|
||||||
|
* @default false
|
||||||
|
*/
|
||||||
|
suspensible?: boolean
|
||||||
}
|
}
|
||||||
|
|
||||||
export const isSuspense = (type: any): boolean => type.__isSuspense
|
export const isSuspense = (type: any): boolean => type.__isSuspense
|
||||||
|
@ -395,7 +401,7 @@ let hasWarned = false
|
||||||
|
|
||||||
function createSuspenseBoundary(
|
function createSuspenseBoundary(
|
||||||
vnode: VNode,
|
vnode: VNode,
|
||||||
parent: SuspenseBoundary | null,
|
parentSuspense: SuspenseBoundary | null,
|
||||||
parentComponent: ComponentInternalInstance | null,
|
parentComponent: ComponentInternalInstance | null,
|
||||||
container: RendererElement,
|
container: RendererElement,
|
||||||
hiddenContainer: RendererElement,
|
hiddenContainer: RendererElement,
|
||||||
|
@ -423,6 +429,17 @@ function createSuspenseBoundary(
|
||||||
o: { parentNode, remove }
|
o: { parentNode, remove }
|
||||||
} = rendererInternals
|
} = 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
|
const timeout = vnode.props ? toNumber(vnode.props.timeout) : undefined
|
||||||
if (__DEV__) {
|
if (__DEV__) {
|
||||||
assertNumber(timeout, `Suspense timeout`)
|
assertNumber(timeout, `Suspense timeout`)
|
||||||
|
@ -430,7 +447,7 @@ function createSuspenseBoundary(
|
||||||
|
|
||||||
const suspense: SuspenseBoundary = {
|
const suspense: SuspenseBoundary = {
|
||||||
vnode,
|
vnode,
|
||||||
parent,
|
parent: parentSuspense,
|
||||||
parentComponent,
|
parentComponent,
|
||||||
isSVG,
|
isSVG,
|
||||||
container,
|
container,
|
||||||
|
@ -522,6 +539,20 @@ function createSuspenseBoundary(
|
||||||
}
|
}
|
||||||
suspense.effects = []
|
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
|
// invoke @resolve event
|
||||||
triggerEvent(vnode, 'onResolve')
|
triggerEvent(vnode, 'onResolve')
|
||||||
},
|
},
|
||||||
|
|
Loading…
Reference in New Issue