wip(suspense): discard side effects when content is unmounted before resolve

This commit is contained in:
Evan You
2019-09-11 13:22:18 -04:00
parent ccfcdb8746
commit 6dc91971d1
4 changed files with 332 additions and 142 deletions

View File

@@ -690,59 +690,146 @@ export function createRenderer<
optimized: boolean
) {
if (n1 == null) {
const suspense = (n2.suspense = createSuspenseBoundary(
mountSuspense(
n2,
container,
anchor,
parentComponent,
parentSuspense,
hostCreateElement('div'),
resolveSuspense
))
isSVG,
optimized
)
} else {
patchSuspense(
n1,
n2,
container,
anchor,
parentComponent,
isSVG,
optimized
)
}
}
function resolveSuspense() {
const { subTree, fallbackTree, effects, vnode } = suspense
// unmount fallback tree
if (fallback.el) {
unmount(fallbackTree as HostVNode, parentComponent, suspense, true)
function mountSuspense(
n2: HostVNode,
container: HostElement,
anchor: HostNode | null,
parentComponent: ComponentInternalInstance | null,
parentSuspense: HostSuspsenseBoundary | null,
isSVG: boolean,
optimized: boolean
) {
const suspense = (n2.suspense = createSuspenseBoundary(
n2,
parentSuspense,
hostCreateElement('div'),
resolveSuspense
))
function resolveSuspense() {
if (__DEV__) {
if (suspense.isResolved) {
throw new Error(
`suspense.resolve() is called when it's already resolved`
)
}
// move content from off-dom container to actual container
move(subTree as HostVNode, container, anchor)
const el = (vnode.el = (subTree 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)
}
// check if there is a pending parent suspense
let parent = suspense.parent
let hasUnresolvedAncestor = false
while (parent) {
if (!parent.isResolved) {
// found a pending parent suspense, merge buffered post jobs
// into that parent
parent.effects.push(...effects)
hasUnresolvedAncestor = true
break
}
}
// no pending parent suspense, flush all jobs
if (!hasUnresolvedAncestor) {
queuePostFlushCb(effects)
}
suspense.isResolved = true
// invoke @resolve event
const onResolve = vnode.props && vnode.props.onResolve
if (isFunction(onResolve)) {
onResolve()
if (suspense.isUnmounted) {
throw new Error(
`suspense.resolve() is called when it's already unmounted`
)
}
}
const { subTree, fallbackTree, effects, vnode } = suspense
// unmount fallback tree
if (fallbackTree.el) {
unmount(fallbackTree as HostVNode, parentComponent, suspense, true)
}
// move content from off-dom container to actual container
move(subTree as HostVNode, container, anchor)
const el = (vnode.el = (subTree 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)
}
// check if there is a pending parent suspense
let parent = suspense.parent
let hasUnresolvedAncestor = false
while (parent) {
if (!parent.isResolved) {
// found a pending parent suspense, merge buffered post jobs
// into that parent
parent.effects.push(...effects)
hasUnresolvedAncestor = true
break
}
}
// no pending parent suspense, flush all jobs
if (!hasUnresolvedAncestor) {
queuePostFlushCb(effects)
}
suspense.isResolved = true
// invoke @resolve event
const onResolve = vnode.props && vnode.props.onResolve
if (isFunction(onResolve)) {
onResolve()
}
}
const { content, fallback } = normalizeSuspenseChildren(n2)
suspense.subTree = content
suspense.fallbackTree = fallback
const { content, fallback } = normalizeSuspenseChildren(n2)
suspense.subTree = content
suspense.fallbackTree = fallback
// start mounting the content subtree in an off-dom container
// TODO should buffer postQueue jobs on current boundary
// start mounting the content subtree in an off-dom container
patch(
null,
content,
suspense.container,
null,
parentComponent,
suspense,
isSVG,
optimized
)
// now check if we have encountered any async deps
if (suspense.deps > 0) {
// mount the fallback tree
patch(
null,
fallback,
container,
anchor,
parentComponent,
null, // fallback tree will not have suspense context
isSVG,
optimized
)
n2.el = fallback.el
} else {
// Suspense has no async deps. Just resolve.
suspense.resolve()
}
}
function patchSuspense(
n1: HostVNode,
n2: HostVNode,
container: HostElement,
anchor: HostNode | null,
parentComponent: ComponentInternalInstance | null,
isSVG: boolean,
optimized: boolean
) {
const suspense = (n2.suspense = n1.suspense) as HostSuspsenseBoundary
suspense.vnode = n2
const { content, fallback } = normalizeSuspenseChildren(n2)
const oldSubTree = suspense.subTree
const oldFallbackTree = suspense.fallbackTree
if (!suspense.isResolved) {
patch(
oldSubTree,
content,
suspense.container,
null,
@@ -751,74 +838,39 @@ export function createRenderer<
isSVG,
optimized
)
// now check if we have encountered any async deps
if (suspense.deps > 0) {
// mount the fallback tree
// still pending. patch the fallback tree.
patch(
null,
oldFallbackTree,
fallback,
container,
anchor,
parentComponent,
suspense,
null, // fallback tree will not have suspense context
isSVG,
optimized
)
n2.el = fallback.el
} else {
// Suspense has no async deps. Just resolve.
suspense.resolve()
}
// If deps somehow becomes 0 after the patch it means the patch caused an
// async dep component to unmount and removed its dep. It will cause the
// suspense to resolve and we don't need to do anything here.
} else {
const suspense = (n2.suspense = n1.suspense) as HostSuspsenseBoundary
suspense.vnode = n2
const { content, fallback } = normalizeSuspenseChildren(n2)
const oldSubTree = (suspense.oldSubTree = suspense.subTree)
suspense.subTree = content
const oldFallbackTree = (suspense.oldFallbackTree = suspense.fallbackTree)
suspense.fallbackTree = fallback
if (!suspense.isResolved) {
patch(
oldSubTree,
content,
suspense.container,
null,
parentComponent,
suspense,
isSVG,
optimized
)
if (suspense.deps > 0) {
// still pending. patch the fallback tree.
patch(
oldFallbackTree,
fallback,
container,
anchor,
parentComponent,
suspense,
isSVG,
optimized
)
n2.el = fallback.el
} else {
suspense.resolve()
}
} else {
// just normal patch inner content as a fragment
patch(
oldSubTree,
content,
container,
anchor,
parentComponent,
suspense,
isSVG,
optimized
)
n2.el = content.el
}
// just normal patch inner content as a fragment
patch(
oldSubTree,
content,
container,
anchor,
parentComponent,
suspense,
isSVG,
optimized
)
n2.el = content.el
}
suspense.subTree = content
suspense.fallbackTree = fallback
}
function processComponent(
@@ -913,10 +965,19 @@ export function createRenderer<
// before proceeding
if (__FEATURE_SUSPENSE__ && instance.asyncDep) {
if (!parentSuspense) {
// TODO handle this properly
throw new Error('Async component without a suspense boundary!')
}
if (parentSuspense.isResolved) {
// TODO if parentSuspense is already resolved it needs to enter waiting
// state again
}
parentSuspense.deps++
instance.asyncDep.then(asyncSetupResult => {
// unmounted before resolve
if (instance.isUnmounted || parentSuspense.isUnmounted) {
return
}
parentSuspense.deps--
// retry from this component
instance.asyncResolved = true
@@ -1481,12 +1542,7 @@ export function createRenderer<
}
if (__FEATURE_SUSPENSE__ && suspense != null) {
unmount(
suspense.subTree as HostVNode,
parentComponent,
parentSuspense,
doRemove
)
unmountSuspense(suspense, parentComponent, parentSuspense, doRemove)
return
}
@@ -1538,15 +1594,58 @@ export function createRenderer<
stop(effects[i])
}
}
stop(update)
unmount(subTree, instance, parentSuspense, doRemove)
// update may be null if a component is unmounted before its async
// setup has resolved.
if (update !== null) {
stop(update)
unmount(subTree, instance, parentSuspense, doRemove)
}
// unmounted hook
if (um !== null) {
queuePostRenderEffect(um, parentSuspense)
// set unmounted after unmounted hooks are fired
queuePostRenderEffect(() => {
instance.isUnmounted = true
}, parentSuspense)
}
queuePostFlushCb(() => {
instance.isUnmounted = true
})
// A component with async dep inside a pending suspense is unmounted before
// its async dep resolves. This should remove the dep from the suspense, and
// cause the suspense to resolve immediately if that was the last dep.
if (
__FEATURE_SUSPENSE__ &&
parentSuspense !== null &&
!parentSuspense.isResolved &&
!parentSuspense.isUnmounted &&
instance.asyncDep !== null &&
!instance.asyncResolved
) {
parentSuspense.deps--
if (parentSuspense.deps === 0) {
parentSuspense.resolve()
}
}
}
function unmountSuspense(
suspense: HostSuspsenseBoundary,
parentComponent: ComponentInternalInstance | null,
parentSuspense: HostSuspsenseBoundary | null,
doRemove?: boolean
) {
suspense.isUnmounted = true
unmount(
suspense.subTree as HostVNode,
parentComponent,
parentSuspense,
doRemove
)
if (!suspense.isResolved) {
unmount(
suspense.fallbackTree as HostVNode,
parentComponent,
parentSuspense,
doRemove
)
}
}