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(
+ `
`,
+ )
+ 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(
+ ``,
+ )
+ 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