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')
|
test.todo('portal inside suspense')
|
||||||
})
|
})
|
||||||
|
@ -729,7 +729,9 @@ export function createRenderer<
|
|||||||
parentComponent,
|
parentComponent,
|
||||||
container,
|
container,
|
||||||
hiddenContainer,
|
hiddenContainer,
|
||||||
anchor
|
anchor,
|
||||||
|
isSVG,
|
||||||
|
optimized
|
||||||
))
|
))
|
||||||
|
|
||||||
const { content, fallback } = normalizeSuspenseChildren(n2)
|
const { content, fallback } = normalizeSuspenseChildren(n2)
|
||||||
@ -831,20 +833,20 @@ export function createRenderer<
|
|||||||
if (__DEV__) {
|
if (__DEV__) {
|
||||||
if (suspense.isResolved) {
|
if (suspense.isResolved) {
|
||||||
throw new Error(
|
throw new Error(
|
||||||
`suspense.resolve() is called when it's already resolved`
|
`resolveSuspense() is called on an already resolved suspense boundary.`
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
if (suspense.isUnmounted) {
|
if (suspense.isUnmounted) {
|
||||||
throw new Error(
|
throw new Error(
|
||||||
`suspense.resolve() is called when it's already unmounted`
|
`resolveSuspense() is called on an already unmounted suspense boundary.`
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
const {
|
const {
|
||||||
|
vnode,
|
||||||
subTree,
|
subTree,
|
||||||
fallbackTree,
|
fallbackTree,
|
||||||
effects,
|
effects,
|
||||||
vnode,
|
|
||||||
parentComponent,
|
parentComponent,
|
||||||
container
|
container
|
||||||
} = suspense
|
} = 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(
|
function processComponent(
|
||||||
n1: HostVNode | null,
|
n1: HostVNode | null,
|
||||||
n2: HostVNode,
|
n2: HostVNode,
|
||||||
@ -986,10 +1029,16 @@ export function createRenderer<
|
|||||||
// TODO handle this properly
|
// TODO handle this properly
|
||||||
throw new Error('Async component without a suspense boundary!')
|
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) {
|
if (parentSuspense.isResolved) {
|
||||||
// TODO if parentSuspense is already resolved it needs to enter waiting
|
queueJob(() => {
|
||||||
// state again
|
restartSuspense(parentSuspense)
|
||||||
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
parentSuspense.deps++
|
parentSuspense.deps++
|
||||||
instance.asyncDep
|
instance.asyncDep
|
||||||
.catch(err => {
|
.catch(err => {
|
||||||
@ -1006,6 +1055,7 @@ export function createRenderer<
|
|||||||
)
|
)
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
|
||||||
// give it a placeholder
|
// give it a placeholder
|
||||||
const placeholder = (instance.subTree = createVNode(Empty))
|
const placeholder = (instance.subTree = createVNode(Empty))
|
||||||
processEmptyNode(null, placeholder, container, anchor)
|
processEmptyNode(null, placeholder, container, anchor)
|
||||||
@ -1118,8 +1168,7 @@ export function createRenderer<
|
|||||||
parentSuspense,
|
parentSuspense,
|
||||||
isSVG
|
isSVG
|
||||||
)
|
)
|
||||||
let current = instance.vnode
|
instance.vnode.el = nextTree.el
|
||||||
current.el = nextTree.el
|
|
||||||
if (next === null) {
|
if (next === null) {
|
||||||
// self-triggered update. In case of HOC, update parent component
|
// self-triggered update. In case of HOC, update parent component
|
||||||
// vnode el. HOC is indicated by parent instance's subTree pointing
|
// vnode el. HOC is indicated by parent instance's subTree pointing
|
||||||
|
@ -13,6 +13,8 @@ export interface SuspenseBoundary<
|
|||||||
vnode: HostVNode
|
vnode: HostVNode
|
||||||
parent: SuspenseBoundary<HostNode, HostElement> | null
|
parent: SuspenseBoundary<HostNode, HostElement> | null
|
||||||
parentComponent: ComponentInternalInstance | null
|
parentComponent: ComponentInternalInstance | null
|
||||||
|
isSVG: boolean
|
||||||
|
optimized: boolean
|
||||||
container: HostElement
|
container: HostElement
|
||||||
hiddenContainer: HostElement
|
hiddenContainer: HostElement
|
||||||
anchor: HostNode | null
|
anchor: HostNode | null
|
||||||
@ -30,12 +32,16 @@ export function createSuspenseBoundary<HostNode, HostElement>(
|
|||||||
parentComponent: ComponentInternalInstance | null,
|
parentComponent: ComponentInternalInstance | null,
|
||||||
container: HostElement,
|
container: HostElement,
|
||||||
hiddenContainer: HostElement,
|
hiddenContainer: HostElement,
|
||||||
anchor: HostNode | null
|
anchor: HostNode | null,
|
||||||
|
isSVG: boolean,
|
||||||
|
optimized: boolean
|
||||||
): SuspenseBoundary<HostNode, HostElement> {
|
): SuspenseBoundary<HostNode, HostElement> {
|
||||||
return {
|
return {
|
||||||
vnode,
|
vnode,
|
||||||
parent,
|
parent,
|
||||||
parentComponent,
|
parentComponent,
|
||||||
|
isSVG,
|
||||||
|
optimized,
|
||||||
container,
|
container,
|
||||||
hiddenContainer,
|
hiddenContainer,
|
||||||
anchor,
|
anchor,
|
||||||
|
Loading…
x
Reference in New Issue
Block a user