diff --git a/packages/runtime-core/__tests__/components/Suspense.spec.ts b/packages/runtime-core/__tests__/components/Suspense.spec.ts index 8de5b3182d0..fe807c6dfaa 100644 --- a/packages/runtime-core/__tests__/components/Suspense.spec.ts +++ b/packages/runtime-core/__tests__/components/Suspense.spec.ts @@ -472,6 +472,124 @@ describe('Suspense', () => { expect(calls).toEqual([]) }) + // #10042 + test('unmount suspensible child before parent suspense resolve', async () => { + const calls: string[] = [] + + const OuterB = defineAsyncComponent( + { + setup: () => { + onMounted(() => { + calls.push('OuterB mounted') + }) + onUnmounted(() => { + calls.push('OuterB unmounted') + }) + return () => h('div', 'OuterB') + }, + }, + 10, + ) + + const OuterA = defineAsyncComponent( + { + setup: () => { + onMounted(() => { + calls.push('OuterA mounted') + }) + onUnmounted(() => { + calls.push('OuterA unmounted') + }) + return () => + h('div', null, [ + h('div', 'OuterA'), + h(RouterView, null, { + default: ({ Component }: any) => [ + Component + ? h( + Suspense, + { suspensible: true }, + { + default: () => h(Component), + }, + ) + : null, + ], + }), + ]) + }, + }, + 10, + ) + + const InnerA = defineAsyncComponent( + { + setup: () => { + onMounted(() => { + calls.push('InnerA mounted') + }) + onUnmounted(() => { + calls.push('InnerA unmounted') + }) + return () => h('div', 'InnerA') + }, + }, + 5, + ) + + const toggle = ref(true) + const route = computed(() => { + return toggle.value ? [OuterA, InnerA] : [OuterB] + }) + + const Comp = { + setup() { + provide('route', route) + return () => + h(RouterView, null, { + default: ({ Component }: any) => [ + h(Suspense, null, { + default: () => h(Component), + }), + ], + }) + }, + } + + const root = nodeOps.createElement('div') + render(h(Comp), root) + await Promise.all(deps) + await nextTick() + expect(serializeInner(root)).toBe(``) + + await Promise.all(deps) + await nextTick() + expect(serializeInner(root)).toBe( + `
OuterA
InnerA
`, + ) + expect(calls).toEqual([`OuterA mounted`, 'InnerA mounted']) + + deps.length = 0 + calls.length = 0 + toggle.value = false + await nextTick() + + // expect not change + expect(serializeInner(root)).toBe( + `
OuterA
InnerA
`, + ) + expect(calls).toEqual([]) + + await Promise.all(deps) + await nextTick() + expect(serializeInner(root)).toBe(`
OuterB
`) + expect(calls).toEqual([ + 'OuterB mounted', + 'InnerA unmounted', + 'OuterA unmounted', + ]) + }) + test('unmount suspense after resolve', async () => { const toggle = ref(true) const unmounted = vi.fn() diff --git a/packages/runtime-core/src/components/Suspense.ts b/packages/runtime-core/src/components/Suspense.ts index e9723f23652..4a43452334a 100644 --- a/packages/runtime-core/src/components/Suspense.ts +++ b/packages/runtime-core/src/components/Suspense.ts @@ -425,6 +425,7 @@ export interface SuspenseBoundary { isInFallback: boolean isHydrating: boolean isUnmounted: boolean + preEffects: Function[] effects: Function[] resolve(force?: boolean, sync?: boolean): void fallback(fallbackVNode: VNode): void @@ -506,6 +507,7 @@ function createSuspenseBoundary( isInFallback: !isHydrating, isHydrating, isUnmounted: false, + preEffects: [], effects: [], resolve(resume = false, sync = false) { @@ -526,6 +528,7 @@ function createSuspenseBoundary( activeBranch, pendingBranch, pendingId, + preEffects, effects, parentComponent, container, @@ -536,6 +539,10 @@ function createSuspenseBoundary( if (suspense.isHydrating) { suspense.isHydrating = false } else if (!resume) { + if (preEffects.length > 0) { + preEffects.forEach(e => e()) + suspense.preEffects = [] + } delayEnter = activeBranch && pendingBranch!.transition && @@ -901,3 +908,15 @@ function isVNodeSuspensible(vnode: VNode) { const suspensible = vnode.props && vnode.props.suspensible return suspensible != null && suspensible !== false } + +export function hasSuspensibleChild(vnode: VNode): boolean { + if (vnode.shapeFlag & ShapeFlags.SUSPENSE && isVNodeSuspensible(vnode)) { + return true + } + + if (isArray(vnode.children)) { + return vnode.children.some(child => hasSuspensibleChild(child as VNode)) + } + + return false +} diff --git a/packages/runtime-core/src/renderer.ts b/packages/runtime-core/src/renderer.ts index d5c5b6d8dfb..cbf65b41706 100644 --- a/packages/runtime-core/src/renderer.ts +++ b/packages/runtime-core/src/renderer.ts @@ -56,6 +56,7 @@ import { setRef } from './rendererTemplateRef' import { type SuspenseBoundary, type SuspenseImpl, + hasSuspensibleChild, queueEffectWithSuspense, } from './components/Suspense' import type { TeleportImpl, TeleportVNode } from './components/Teleport' @@ -1583,8 +1584,22 @@ function baseCreateRenderer( )) const update: SchedulerJob = (instance.update = () => { - if (effect.dirty) { - effect.run() + const job = () => { + if (effect.dirty) { + effect.run() + } + } + + if ( + __FEATURE_SUSPENSE__ && + parentSuspense && + parentSuspense.deps > 0 && + instance.subTree && + hasSuspensibleChild(instance.subTree) + ) { + parentSuspense.preEffects.push(job) + } else { + job() } }) update.id = instance.uid