feat: re-suspense when encountering new async deps in resolved state
This commit is contained in:
parent
4b3567035a
commit
1c628d0b79
@ -658,7 +658,49 @@ describe('renderer: suspense', () => {
|
||||
)
|
||||
})
|
||||
|
||||
test.todo('new async dep after resolve should cause suspense to restart')
|
||||
test('new async dep after resolve should cause suspense to restart', async () => {
|
||||
const toggle = ref(false)
|
||||
|
||||
const ChildA = createAsyncComponent({
|
||||
setup() {
|
||||
return () => h('div', 'Child A')
|
||||
}
|
||||
})
|
||||
|
||||
const ChildB = createAsyncComponent({
|
||||
setup() {
|
||||
return () => h('div', 'Child B')
|
||||
}
|
||||
})
|
||||
|
||||
const Comp = {
|
||||
setup() {
|
||||
return () =>
|
||||
h(Suspense, null, {
|
||||
default: [h(ChildA), toggle.value ? h(ChildB) : null],
|
||||
fallback: h('div', 'root fallback')
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
const root = nodeOps.createElement('div')
|
||||
render(h(Comp), root)
|
||||
expect(serializeInner(root)).toBe(`<div>root fallback</div>`)
|
||||
|
||||
await deps[0]
|
||||
await nextTick()
|
||||
expect(serializeInner(root)).toBe(`<!----><div>Child A</div><!----><!---->`)
|
||||
|
||||
toggle.value = true
|
||||
await nextTick()
|
||||
expect(serializeInner(root)).toBe(`<div>root fallback</div>`)
|
||||
|
||||
await deps[1]
|
||||
await nextTick()
|
||||
expect(serializeInner(root)).toBe(
|
||||
`<!----><div>Child A</div><div>Child B</div><!---->`
|
||||
)
|
||||
})
|
||||
|
||||
test.todo('portal inside suspense')
|
||||
})
|
||||
|
@ -729,7 +729,9 @@ export function createRenderer<
|
||||
parentComponent,
|
||||
container,
|
||||
hiddenContainer,
|
||||
anchor
|
||||
anchor,
|
||||
isSVG,
|
||||
optimized
|
||||
))
|
||||
|
||||
const { content, fallback } = normalizeSuspenseChildren(n2)
|
||||
@ -831,20 +833,20 @@ export function createRenderer<
|
||||
if (__DEV__) {
|
||||
if (suspense.isResolved) {
|
||||
throw new Error(
|
||||
`suspense.resolve() is called when it's already resolved`
|
||||
`resolveSuspense() is called on an already resolved suspense boundary.`
|
||||
)
|
||||
}
|
||||
if (suspense.isUnmounted) {
|
||||
throw new Error(
|
||||
`suspense.resolve() is called when it's already unmounted`
|
||||
`resolveSuspense() is called on an already unmounted suspense boundary.`
|
||||
)
|
||||
}
|
||||
}
|
||||
const {
|
||||
vnode,
|
||||
subTree,
|
||||
fallbackTree,
|
||||
effects,
|
||||
vnode,
|
||||
parentComponent,
|
||||
container
|
||||
} = suspense
|
||||
@ -891,6 +893,47 @@ export function createRenderer<
|
||||
}
|
||||
}
|
||||
|
||||
function restartSuspense(suspense: HostSuspsenseBoundary) {
|
||||
suspense.isResolved = false
|
||||
const {
|
||||
vnode,
|
||||
subTree,
|
||||
fallbackTree,
|
||||
parentComponent,
|
||||
container,
|
||||
hiddenContainer,
|
||||
isSVG,
|
||||
optimized
|
||||
} = suspense
|
||||
|
||||
// move content tree back to the off-dom container
|
||||
const anchor = getNextHostNode(subTree)
|
||||
move(subTree as HostVNode, hiddenContainer, null)
|
||||
// remount the fallback tree
|
||||
patch(
|
||||
null,
|
||||
fallbackTree,
|
||||
container,
|
||||
anchor,
|
||||
parentComponent,
|
||||
null, // fallback tree will not have suspense context
|
||||
isSVG,
|
||||
optimized
|
||||
)
|
||||
const el = (vnode.el = (fallbackTree as HostVNode).el as HostNode)
|
||||
// suspense as the root node of a component...
|
||||
if (parentComponent && parentComponent.subTree === vnode) {
|
||||
parentComponent.vnode.el = el
|
||||
updateHOCHostEl(parentComponent, el)
|
||||
}
|
||||
|
||||
// invoke @suspense event
|
||||
const onSuspense = vnode.props && vnode.props.onSuspense
|
||||
if (isFunction(onSuspense)) {
|
||||
onSuspense()
|
||||
}
|
||||
}
|
||||
|
||||
function processComponent(
|
||||
n1: HostVNode | null,
|
||||
n2: HostVNode,
|
||||
@ -986,10 +1029,16 @@ export function createRenderer<
|
||||
// TODO handle this properly
|
||||
throw new Error('Async component without a suspense boundary!')
|
||||
}
|
||||
|
||||
// parent suspense already resolved, need to re-suspense
|
||||
// use queueJob so it's handled synchronously after patching the current
|
||||
// suspense tree
|
||||
if (parentSuspense.isResolved) {
|
||||
// TODO if parentSuspense is already resolved it needs to enter waiting
|
||||
// state again
|
||||
queueJob(() => {
|
||||
restartSuspense(parentSuspense)
|
||||
})
|
||||
}
|
||||
|
||||
parentSuspense.deps++
|
||||
instance.asyncDep
|
||||
.catch(err => {
|
||||
@ -1006,6 +1055,7 @@ export function createRenderer<
|
||||
)
|
||||
}
|
||||
})
|
||||
|
||||
// give it a placeholder
|
||||
const placeholder = (instance.subTree = createVNode(Empty))
|
||||
processEmptyNode(null, placeholder, container, anchor)
|
||||
@ -1118,8 +1168,7 @@ export function createRenderer<
|
||||
parentSuspense,
|
||||
isSVG
|
||||
)
|
||||
let current = instance.vnode
|
||||
current.el = nextTree.el
|
||||
instance.vnode.el = nextTree.el
|
||||
if (next === null) {
|
||||
// self-triggered update. In case of HOC, update parent component
|
||||
// vnode el. HOC is indicated by parent instance's subTree pointing
|
||||
|
@ -13,6 +13,8 @@ export interface SuspenseBoundary<
|
||||
vnode: HostVNode
|
||||
parent: SuspenseBoundary<HostNode, HostElement> | null
|
||||
parentComponent: ComponentInternalInstance | null
|
||||
isSVG: boolean
|
||||
optimized: boolean
|
||||
container: HostElement
|
||||
hiddenContainer: HostElement
|
||||
anchor: HostNode | null
|
||||
@ -30,12 +32,16 @@ export function createSuspenseBoundary<HostNode, HostElement>(
|
||||
parentComponent: ComponentInternalInstance | null,
|
||||
container: HostElement,
|
||||
hiddenContainer: HostElement,
|
||||
anchor: HostNode | null
|
||||
anchor: HostNode | null,
|
||||
isSVG: boolean,
|
||||
optimized: boolean
|
||||
): SuspenseBoundary<HostNode, HostElement> {
|
||||
return {
|
||||
vnode,
|
||||
parent,
|
||||
parentComponent,
|
||||
isSVG,
|
||||
optimized,
|
||||
container,
|
||||
hiddenContainer,
|
||||
anchor,
|
||||
|
Loading…
x
Reference in New Issue
Block a user