feat: re-suspense when encountering new async deps in resolved state

This commit is contained in:
Evan You 2019-09-12 12:16:01 -04:00
parent 4b3567035a
commit 1c628d0b79
3 changed files with 107 additions and 10 deletions

View File

@ -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')
})

View File

@ -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

View File

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