diff --git a/packages/runtime-core/__tests__/components/KeepAlive.spec.ts b/packages/runtime-core/__tests__/components/KeepAlive.spec.ts index 1e4176b6fb8..7214e6c2e80 100644 --- a/packages/runtime-core/__tests__/components/KeepAlive.spec.ts +++ b/packages/runtime-core/__tests__/components/KeepAlive.spec.ts @@ -1173,4 +1173,48 @@ describe('KeepAlive', () => { expect(deactivatedHome).toHaveBeenCalledTimes(0) expect(unmountedHome).toHaveBeenCalledTimes(1) }) + + // #12017 + test('avoid duplicate mounts of deactivate components', async () => { + const About = { + name: 'About', + setup() { + return () => h('h1', 'About') + }, + } + const mountedHome = vi.fn() + const Home = { + name: 'Home', + setup() { + onMounted(mountedHome) + return () => h('h1', 'Home') + }, + } + const activeView = shallowRef(About) + const HomeView = { + name: 'HomeView', + setup() { + return () => h(activeView.value) + }, + } + + const App = createApp({ + setup() { + return () => { + return [ + h(KeepAlive, null, [ + h(HomeView, { + key: activeView.value.name, + }), + ]), + ] + } + }, + }) + App.mount(nodeOps.createElement('div')) + expect(mountedHome).toHaveBeenCalledTimes(0) + activeView.value = Home + await nextTick() + expect(mountedHome).toHaveBeenCalledTimes(1) + }) }) diff --git a/packages/runtime-core/src/component.ts b/packages/runtime-core/src/component.ts index a1ce1de4eb9..4137dfd41a2 100644 --- a/packages/runtime-core/src/component.ts +++ b/packages/runtime-core/src/component.ts @@ -506,9 +506,12 @@ export interface ComponentInternalInstance { */ asyncResolved: boolean + keepAliveEffect: Function[] + // lifecycle isMounted: boolean isUnmounted: boolean + isActivated: boolean isDeactivated: boolean /** * @internal @@ -669,10 +672,13 @@ export function createComponentInstance( asyncDep: null, asyncResolved: false, + keepAliveEffect: [], + // lifecycle hooks // not using enums here because it results in computed properties isMounted: false, isUnmounted: false, + isActivated: false, isDeactivated: false, bc: null, c: null, diff --git a/packages/runtime-core/src/components/KeepAlive.ts b/packages/runtime-core/src/components/KeepAlive.ts index 5976f3a4b33..bc77cf9ba3f 100644 --- a/packages/runtime-core/src/components/KeepAlive.ts +++ b/packages/runtime-core/src/components/KeepAlive.ts @@ -48,6 +48,7 @@ import { devtoolsComponentAdded } from '../devtools' import { isAsyncWrapper } from '../apiAsyncComponent' import { isSuspense } from './Suspense' import { LifecycleHooks } from '../enums' +import { queuePostFlushCb } from '../scheduler' type MatchPattern = string | RegExp | (string | RegExp)[] @@ -136,6 +137,7 @@ const KeepAliveImpl: ComponentOptions = { optimized, ) => { const instance = vnode.component! + instance.isActivated = false move(vnode, container, anchor, MoveType.ENTER, parentSuspense) // in case props have changed patch( @@ -149,6 +151,11 @@ const KeepAliveImpl: ComponentOptions = { vnode.slotScopeIds, optimized, ) + + const effects = instance.keepAliveEffect + queuePostFlushCb(effects) + instance.keepAliveEffect.length = 0 + queuePostRenderEffect(() => { instance.isDeactivated = false if (instance.a) { @@ -168,6 +175,7 @@ const KeepAliveImpl: ComponentOptions = { sharedContext.deactivate = (vnode: VNode) => { const instance = vnode.component! + instance.isActivated = true invalidateMount(instance.m) invalidateMount(instance.a) diff --git a/packages/runtime-core/src/renderer.ts b/packages/runtime-core/src/renderer.ts index 90cc22f5470..db0efe83890 100644 --- a/packages/runtime-core/src/renderer.ts +++ b/packages/runtime-core/src/renderer.ts @@ -1431,6 +1431,16 @@ function baseCreateRenderer( } else { let { next, bu, u, parent, vnode } = instance + const keepAliveParent = locateDeactiveKeepAlive(instance) + if (keepAliveParent) { + keepAliveParent.keepAliveEffect.push(() => { + if (!instance.isUnmounted) { + componentUpdateFn() + } + }) + return + } + if (__FEATURE_SUSPENSE__) { const nonHydratedAsyncRoot = locateNonHydratedAsyncRoot(instance) // we are trying to update some async comp before hydration @@ -2542,6 +2552,19 @@ function locateNonHydratedAsyncRoot( } } +function locateDeactiveKeepAlive(instance: ComponentInternalInstance | null) { + while (instance) { + if (instance.isActivated) { + return instance + } + if (isKeepAlive(instance.vnode)) { + break + } + instance = instance.parent + } + return null +} + export function invalidateMount(hooks: LifecycleHook): void { if (hooks) { for (let i = 0; i < hooks.length; i++)