From 5e4df55bced8676dbb088bba209fada0d13ff284 Mon Sep 17 00:00:00 2001 From: linzhe141 <1572213544@qq.com> Date: Wed, 12 Nov 2025 15:16:23 +0800 Subject: [PATCH 1/3] fix(suspense): defer clearing fallback vnode reference to should unmount active branch first --- .../runtime-core/src/components/Suspense.ts | 17 +++++++++++------ 1 file changed, 11 insertions(+), 6 deletions(-) diff --git a/packages/runtime-core/src/components/Suspense.ts b/packages/runtime-core/src/components/Suspense.ts index d14e96be3d0..4731e939409 100644 --- a/packages/runtime-core/src/components/Suspense.ts +++ b/packages/runtime-core/src/components/Suspense.ts @@ -20,6 +20,7 @@ import { type RendererInternals, type RendererNode, type SetupRenderEffectFn, + queuePostRenderEffect, } from '../renderer' import { queuePostFlushCb } from '../scheduler' import { filterSingleRoot, updateHOCHostEl } from '../componentRenderUtils' @@ -553,9 +554,11 @@ function createSuspenseBoundary( ) queuePostFlushCb(effects) // clear el reference from fallback vnode to allow GC after transition - if (isInFallback && vnode.ssFallback) { - vnode.ssFallback.el = null - } + queuePostRenderEffect(() => { + if (isInFallback && vnode.ssFallback) { + vnode.ssFallback.el = null + } + }, suspense) } } } @@ -577,9 +580,11 @@ function createSuspenseBoundary( unmount(activeBranch, parentComponent, suspense, true) // clear el reference from fallback vnode to allow GC // only clear immediately if there's no delayed transition - if (!delayEnter && isInFallback && vnode.ssFallback) { - vnode.ssFallback.el = null - } + queuePostRenderEffect(() => { + if (!delayEnter && isInFallback && vnode.ssFallback) { + vnode.ssFallback.el = null + } + }, suspense) } if (!delayEnter) { // move content from off-dom container to actual container From f6ec7757cd50fff9c325dd192d14a616f24b4a24 Mon Sep 17 00:00:00 2001 From: linzhe141 <1572213544@qq.com> Date: Wed, 12 Nov 2025 17:40:18 +0800 Subject: [PATCH 2/3] chore: add unit test --- .../__tests__/components/Suspense.spec.ts | 36 +++++++++++++++++++ .../runtime-core/src/components/Suspense.ts | 8 ++--- 2 files changed, 39 insertions(+), 5 deletions(-) diff --git a/packages/runtime-core/__tests__/components/Suspense.spec.ts b/packages/runtime-core/__tests__/components/Suspense.spec.ts index c6683d6a257..4e8da3288f1 100644 --- a/packages/runtime-core/__tests__/components/Suspense.spec.ts +++ b/packages/runtime-core/__tests__/components/Suspense.spec.ts @@ -24,6 +24,7 @@ import { shallowRef, watch, watchEffect, + withDirectives, } from '@vue/runtime-test' import { computed, createApp, defineComponent, inject, provide } from 'vue' import type { RawSlots } from 'packages/runtime-core/src/componentSlots' @@ -2358,5 +2359,40 @@ describe('Suspense', () => { `
444
555
666
`, ) }) + + test('should call unmounted directive once when fallback is replaced by resolved async component', async () => { + const Comp = { + render() { + return h('div', null, 'comp') + }, + } + const Foo = defineAsyncComponent({ + render() { + return h(Comp) + }, + }) + const unmounted = vi.fn(el => { + el.foo = null + }) + const vDir = { + unmounted, + } + const App = { + setup() { + return () => { + return h(Suspense, null, { + fallback: () => withDirectives(h('div'), [[vDir, true]]), + default: () => h(Foo), + }) + } + }, + } + const root = nodeOps.createElement('div') + render(h(App), root) + + await Promise.all(deps) + await nextTick() + expect(unmounted).toHaveBeenCalledTimes(1) + }) }) }) diff --git a/packages/runtime-core/src/components/Suspense.ts b/packages/runtime-core/src/components/Suspense.ts index 4731e939409..54bf8edb289 100644 --- a/packages/runtime-core/src/components/Suspense.ts +++ b/packages/runtime-core/src/components/Suspense.ts @@ -554,11 +554,9 @@ function createSuspenseBoundary( ) queuePostFlushCb(effects) // clear el reference from fallback vnode to allow GC after transition - queuePostRenderEffect(() => { - if (isInFallback && vnode.ssFallback) { - vnode.ssFallback.el = null - } - }, suspense) + if (isInFallback && vnode.ssFallback) { + vnode.ssFallback.el = null + } } } } From 0a9d60a06b297c4b5550f9c6dafc5f279f160e34 Mon Sep 17 00:00:00 2001 From: daiwei Date: Wed, 12 Nov 2025 20:27:56 +0800 Subject: [PATCH 3/3] chore: tweaks --- packages/runtime-core/src/components/Suspense.ts | 9 +++------ 1 file changed, 3 insertions(+), 6 deletions(-) diff --git a/packages/runtime-core/src/components/Suspense.ts b/packages/runtime-core/src/components/Suspense.ts index 54bf8edb289..0c8b6c28e8d 100644 --- a/packages/runtime-core/src/components/Suspense.ts +++ b/packages/runtime-core/src/components/Suspense.ts @@ -577,12 +577,9 @@ function createSuspenseBoundary( } unmount(activeBranch, parentComponent, suspense, true) // clear el reference from fallback vnode to allow GC - // only clear immediately if there's no delayed transition - queuePostRenderEffect(() => { - if (!delayEnter && isInFallback && vnode.ssFallback) { - vnode.ssFallback.el = null - } - }, suspense) + if (!delayEnter && isInFallback && vnode.ssFallback) { + queuePostRenderEffect(() => (vnode.ssFallback!.el = null), suspense) + } } if (!delayEnter) { // move content from off-dom container to actual container