wip(suspense): discard side effects when content is unmounted before resolve
This commit is contained in:
parent
ccfcdb8746
commit
6dc91971d1
@ -164,13 +164,7 @@ describe('renderer: suspense', () => {
|
|||||||
})
|
})
|
||||||
|
|
||||||
await p
|
await p
|
||||||
// test resume for returning bindings
|
return () => h('div', 'async')
|
||||||
return {
|
|
||||||
msg: 'async'
|
|
||||||
}
|
|
||||||
},
|
|
||||||
render(this: any) {
|
|
||||||
return h('div', this.msg)
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -201,11 +195,100 @@ describe('renderer: suspense', () => {
|
|||||||
expect(calls).toEqual([`watch callback`, `mounted`, 'unmounted'])
|
expect(calls).toEqual([`watch callback`, `mounted`, 'unmounted'])
|
||||||
})
|
})
|
||||||
|
|
||||||
// should receive updated props/slots when resolved
|
test('content update before suspense resolve', async () => {
|
||||||
test.todo('content update before suspense resolve')
|
const Async = createAsyncComponent({
|
||||||
|
setup(props: { msg: string }) {
|
||||||
|
return () => h('div', props.msg)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
const msg = ref('foo')
|
||||||
|
|
||||||
|
const Comp = {
|
||||||
|
setup() {
|
||||||
|
return () =>
|
||||||
|
h(Suspense, null, {
|
||||||
|
default: h(Async, { msg: msg.value }),
|
||||||
|
fallback: h('div', `fallback ${msg.value}`)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const root = nodeOps.createElement('div')
|
||||||
|
render(h(Comp), root)
|
||||||
|
expect(serializeInner(root)).toBe(`<div>fallback foo</div>`)
|
||||||
|
|
||||||
|
// value changed before resolve
|
||||||
|
msg.value = 'bar'
|
||||||
|
await nextTick()
|
||||||
|
// fallback content should be updated
|
||||||
|
expect(serializeInner(root)).toBe(`<div>fallback bar</div>`)
|
||||||
|
|
||||||
|
await Promise.all(deps)
|
||||||
|
await nextTick()
|
||||||
|
// async component should receive updated props/slots when resolved
|
||||||
|
expect(serializeInner(root)).toBe(`<div>bar</div>`)
|
||||||
|
})
|
||||||
|
|
||||||
// mount/unmount hooks should not even fire
|
// mount/unmount hooks should not even fire
|
||||||
test.todo('unmount before suspense resolve')
|
test('unmount before suspense resolve', async () => {
|
||||||
|
const deps: Promise<any>[] = []
|
||||||
|
const calls: string[] = []
|
||||||
|
const toggle = ref(true)
|
||||||
|
|
||||||
|
const Async = {
|
||||||
|
async setup() {
|
||||||
|
const p = new Promise(r => setTimeout(r, 1))
|
||||||
|
deps.push(p)
|
||||||
|
|
||||||
|
watch(() => {
|
||||||
|
calls.push('watch callback')
|
||||||
|
})
|
||||||
|
|
||||||
|
onMounted(() => {
|
||||||
|
calls.push('mounted')
|
||||||
|
})
|
||||||
|
|
||||||
|
onUnmounted(() => {
|
||||||
|
calls.push('unmounted')
|
||||||
|
})
|
||||||
|
|
||||||
|
await p
|
||||||
|
return () => h('div', 'async')
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const Comp = {
|
||||||
|
setup() {
|
||||||
|
return () =>
|
||||||
|
h(Suspense, null, {
|
||||||
|
default: toggle.value ? h(Async) : null,
|
||||||
|
fallback: h('div', 'fallback')
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const root = nodeOps.createElement('div')
|
||||||
|
render(h(Comp), root)
|
||||||
|
expect(serializeInner(root)).toBe(`<div>fallback</div>`)
|
||||||
|
expect(calls).toEqual([])
|
||||||
|
|
||||||
|
// remvoe the async dep before it's resolved
|
||||||
|
toggle.value = false
|
||||||
|
await nextTick()
|
||||||
|
// should cause the suspense to resolve immediately
|
||||||
|
expect(serializeInner(root)).toBe(`<!---->`)
|
||||||
|
|
||||||
|
await Promise.all(deps)
|
||||||
|
await nextTick()
|
||||||
|
expect(serializeInner(root)).toBe(`<!---->`)
|
||||||
|
// should discard effects
|
||||||
|
expect(calls).toEqual([])
|
||||||
|
})
|
||||||
|
|
||||||
|
test.todo('unmount suspense after resolve')
|
||||||
|
|
||||||
|
test.todo('unmount suspense before resolve')
|
||||||
|
|
||||||
test.todo('nested suspense')
|
test.todo('nested suspense')
|
||||||
|
|
||||||
|
@ -113,6 +113,9 @@ function doWatch(
|
|||||||
} else {
|
} else {
|
||||||
// no cb -> simple effect
|
// no cb -> simple effect
|
||||||
getter = () => {
|
getter = () => {
|
||||||
|
if (instance && instance.isUnmounted) {
|
||||||
|
return
|
||||||
|
}
|
||||||
if (cleanup) {
|
if (cleanup) {
|
||||||
cleanup()
|
cleanup()
|
||||||
}
|
}
|
||||||
@ -141,6 +144,9 @@ function doWatch(
|
|||||||
let oldValue = isArray(source) ? [] : undefined
|
let oldValue = isArray(source) ? [] : undefined
|
||||||
const applyCb = cb
|
const applyCb = cb
|
||||||
? () => {
|
? () => {
|
||||||
|
if (instance && instance.isUnmounted) {
|
||||||
|
return
|
||||||
|
}
|
||||||
const newValue = runner()
|
const newValue = runner()
|
||||||
if (deep || newValue !== oldValue) {
|
if (deep || newValue !== oldValue) {
|
||||||
// cleanup before running cb again
|
// cleanup before running cb again
|
||||||
@ -157,11 +163,11 @@ function doWatch(
|
|||||||
}
|
}
|
||||||
: void 0
|
: void 0
|
||||||
|
|
||||||
const scheduler =
|
let scheduler: (job: () => any) => void
|
||||||
flush === 'sync'
|
if (flush === 'sync') {
|
||||||
? invoke
|
scheduler = invoke
|
||||||
: flush === 'pre'
|
} else if (flush === 'pre') {
|
||||||
? (job: () => any) => {
|
scheduler = job => {
|
||||||
if (!instance || instance.vnode.el != null) {
|
if (!instance || instance.vnode.el != null) {
|
||||||
queueJob(job)
|
queueJob(job)
|
||||||
} else {
|
} else {
|
||||||
@ -170,7 +176,11 @@ function doWatch(
|
|||||||
job()
|
job()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
: (job: () => any) => queuePostRenderEffect(job, suspense)
|
} else {
|
||||||
|
scheduler = job => {
|
||||||
|
queuePostRenderEffect(job, suspense)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
const runner = effect(getter, {
|
const runner = effect(getter, {
|
||||||
lazy: true,
|
lazy: true,
|
||||||
|
@ -690,6 +690,37 @@ export function createRenderer<
|
|||||||
optimized: boolean
|
optimized: boolean
|
||||||
) {
|
) {
|
||||||
if (n1 == null) {
|
if (n1 == null) {
|
||||||
|
mountSuspense(
|
||||||
|
n2,
|
||||||
|
container,
|
||||||
|
anchor,
|
||||||
|
parentComponent,
|
||||||
|
parentSuspense,
|
||||||
|
isSVG,
|
||||||
|
optimized
|
||||||
|
)
|
||||||
|
} else {
|
||||||
|
patchSuspense(
|
||||||
|
n1,
|
||||||
|
n2,
|
||||||
|
container,
|
||||||
|
anchor,
|
||||||
|
parentComponent,
|
||||||
|
isSVG,
|
||||||
|
optimized
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function mountSuspense(
|
||||||
|
n2: HostVNode,
|
||||||
|
container: HostElement,
|
||||||
|
anchor: HostNode | null,
|
||||||
|
parentComponent: ComponentInternalInstance | null,
|
||||||
|
parentSuspense: HostSuspsenseBoundary | null,
|
||||||
|
isSVG: boolean,
|
||||||
|
optimized: boolean
|
||||||
|
) {
|
||||||
const suspense = (n2.suspense = createSuspenseBoundary(
|
const suspense = (n2.suspense = createSuspenseBoundary(
|
||||||
n2,
|
n2,
|
||||||
parentSuspense,
|
parentSuspense,
|
||||||
@ -698,9 +729,21 @@ export function createRenderer<
|
|||||||
))
|
))
|
||||||
|
|
||||||
function resolveSuspense() {
|
function resolveSuspense() {
|
||||||
|
if (__DEV__) {
|
||||||
|
if (suspense.isResolved) {
|
||||||
|
throw new Error(
|
||||||
|
`suspense.resolve() is called when it's already resolved`
|
||||||
|
)
|
||||||
|
}
|
||||||
|
if (suspense.isUnmounted) {
|
||||||
|
throw new Error(
|
||||||
|
`suspense.resolve() is called when it's already unmounted`
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
const { subTree, fallbackTree, effects, vnode } = suspense
|
const { subTree, fallbackTree, effects, vnode } = suspense
|
||||||
// unmount fallback tree
|
// unmount fallback tree
|
||||||
if (fallback.el) {
|
if (fallbackTree.el) {
|
||||||
unmount(fallbackTree as HostVNode, parentComponent, suspense, true)
|
unmount(fallbackTree as HostVNode, parentComponent, suspense, true)
|
||||||
}
|
}
|
||||||
// move content from off-dom container to actual container
|
// move content from off-dom container to actual container
|
||||||
@ -740,7 +783,6 @@ export function createRenderer<
|
|||||||
suspense.fallbackTree = fallback
|
suspense.fallbackTree = fallback
|
||||||
|
|
||||||
// start mounting the content subtree in an off-dom container
|
// start mounting the content subtree in an off-dom container
|
||||||
// TODO should buffer postQueue jobs on current boundary
|
|
||||||
patch(
|
patch(
|
||||||
null,
|
null,
|
||||||
content,
|
content,
|
||||||
@ -760,7 +802,7 @@ export function createRenderer<
|
|||||||
container,
|
container,
|
||||||
anchor,
|
anchor,
|
||||||
parentComponent,
|
parentComponent,
|
||||||
suspense,
|
null, // fallback tree will not have suspense context
|
||||||
isSVG,
|
isSVG,
|
||||||
optimized
|
optimized
|
||||||
)
|
)
|
||||||
@ -769,14 +811,22 @@ export function createRenderer<
|
|||||||
// Suspense has no async deps. Just resolve.
|
// Suspense has no async deps. Just resolve.
|
||||||
suspense.resolve()
|
suspense.resolve()
|
||||||
}
|
}
|
||||||
} else {
|
}
|
||||||
|
|
||||||
|
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
|
const suspense = (n2.suspense = n1.suspense) as HostSuspsenseBoundary
|
||||||
suspense.vnode = n2
|
suspense.vnode = n2
|
||||||
const { content, fallback } = normalizeSuspenseChildren(n2)
|
const { content, fallback } = normalizeSuspenseChildren(n2)
|
||||||
const oldSubTree = (suspense.oldSubTree = suspense.subTree)
|
const oldSubTree = suspense.subTree
|
||||||
suspense.subTree = content
|
const oldFallbackTree = suspense.fallbackTree
|
||||||
const oldFallbackTree = (suspense.oldFallbackTree = suspense.fallbackTree)
|
|
||||||
suspense.fallbackTree = fallback
|
|
||||||
if (!suspense.isResolved) {
|
if (!suspense.isResolved) {
|
||||||
patch(
|
patch(
|
||||||
oldSubTree,
|
oldSubTree,
|
||||||
@ -796,14 +846,15 @@ export function createRenderer<
|
|||||||
container,
|
container,
|
||||||
anchor,
|
anchor,
|
||||||
parentComponent,
|
parentComponent,
|
||||||
suspense,
|
null, // fallback tree will not have suspense context
|
||||||
isSVG,
|
isSVG,
|
||||||
optimized
|
optimized
|
||||||
)
|
)
|
||||||
n2.el = fallback.el
|
n2.el = fallback.el
|
||||||
} else {
|
|
||||||
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 {
|
} else {
|
||||||
// just normal patch inner content as a fragment
|
// just normal patch inner content as a fragment
|
||||||
patch(
|
patch(
|
||||||
@ -818,7 +869,8 @@ export function createRenderer<
|
|||||||
)
|
)
|
||||||
n2.el = content.el
|
n2.el = content.el
|
||||||
}
|
}
|
||||||
}
|
suspense.subTree = content
|
||||||
|
suspense.fallbackTree = fallback
|
||||||
}
|
}
|
||||||
|
|
||||||
function processComponent(
|
function processComponent(
|
||||||
@ -913,10 +965,19 @@ export function createRenderer<
|
|||||||
// before proceeding
|
// before proceeding
|
||||||
if (__FEATURE_SUSPENSE__ && instance.asyncDep) {
|
if (__FEATURE_SUSPENSE__ && instance.asyncDep) {
|
||||||
if (!parentSuspense) {
|
if (!parentSuspense) {
|
||||||
|
// TODO handle this properly
|
||||||
throw new Error('Async component without a suspense boundary!')
|
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++
|
parentSuspense.deps++
|
||||||
instance.asyncDep.then(asyncSetupResult => {
|
instance.asyncDep.then(asyncSetupResult => {
|
||||||
|
// unmounted before resolve
|
||||||
|
if (instance.isUnmounted || parentSuspense.isUnmounted) {
|
||||||
|
return
|
||||||
|
}
|
||||||
parentSuspense.deps--
|
parentSuspense.deps--
|
||||||
// retry from this component
|
// retry from this component
|
||||||
instance.asyncResolved = true
|
instance.asyncResolved = true
|
||||||
@ -1481,12 +1542,7 @@ export function createRenderer<
|
|||||||
}
|
}
|
||||||
|
|
||||||
if (__FEATURE_SUSPENSE__ && suspense != null) {
|
if (__FEATURE_SUSPENSE__ && suspense != null) {
|
||||||
unmount(
|
unmountSuspense(suspense, parentComponent, parentSuspense, doRemove)
|
||||||
suspense.subTree as HostVNode,
|
|
||||||
parentComponent,
|
|
||||||
parentSuspense,
|
|
||||||
doRemove
|
|
||||||
)
|
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -1538,15 +1594,58 @@ export function createRenderer<
|
|||||||
stop(effects[i])
|
stop(effects[i])
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
// update may be null if a component is unmounted before its async
|
||||||
|
// setup has resolved.
|
||||||
|
if (update !== null) {
|
||||||
stop(update)
|
stop(update)
|
||||||
unmount(subTree, instance, parentSuspense, doRemove)
|
unmount(subTree, instance, parentSuspense, doRemove)
|
||||||
|
}
|
||||||
// unmounted hook
|
// unmounted hook
|
||||||
if (um !== null) {
|
if (um !== null) {
|
||||||
queuePostRenderEffect(um, parentSuspense)
|
queuePostRenderEffect(um, parentSuspense)
|
||||||
// set unmounted after unmounted hooks are fired
|
}
|
||||||
queuePostRenderEffect(() => {
|
queuePostFlushCb(() => {
|
||||||
instance.isUnmounted = true
|
instance.isUnmounted = true
|
||||||
}, parentSuspense)
|
})
|
||||||
|
|
||||||
|
// 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
|
||||||
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -12,12 +12,11 @@ export interface SuspenseBoundary<
|
|||||||
vnode: HostVNode
|
vnode: HostVNode
|
||||||
parent: SuspenseBoundary<HostNode, HostElement> | null
|
parent: SuspenseBoundary<HostNode, HostElement> | null
|
||||||
container: HostElement
|
container: HostElement
|
||||||
subTree: HostVNode | null
|
subTree: HostVNode
|
||||||
oldSubTree: HostVNode | null
|
fallbackTree: HostVNode
|
||||||
fallbackTree: HostVNode | null
|
|
||||||
oldFallbackTree: HostVNode | null
|
|
||||||
deps: number
|
deps: number
|
||||||
isResolved: boolean
|
isResolved: boolean
|
||||||
|
isUnmounted: boolean
|
||||||
effects: Function[]
|
effects: Function[]
|
||||||
resolve(): void
|
resolve(): void
|
||||||
}
|
}
|
||||||
@ -33,11 +32,10 @@ export function createSuspenseBoundary<HostNode, HostElement>(
|
|||||||
parent,
|
parent,
|
||||||
container,
|
container,
|
||||||
deps: 0,
|
deps: 0,
|
||||||
subTree: null,
|
subTree: null as any, // will be set immediately after creation
|
||||||
oldSubTree: null,
|
fallbackTree: null as any, // will be set immediately after creation
|
||||||
fallbackTree: null,
|
|
||||||
oldFallbackTree: null,
|
|
||||||
isResolved: false,
|
isResolved: false,
|
||||||
|
isUnmounted: false,
|
||||||
effects: [],
|
effects: [],
|
||||||
resolve
|
resolve
|
||||||
}
|
}
|
||||||
|
Loading…
Reference in New Issue
Block a user