diff --git a/packages/runtime-core/__tests__/components/KeepAlive.spec.ts b/packages/runtime-core/__tests__/components/KeepAlive.spec.ts index 1e4176b6f..7214e6c2e 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 5b094a0d6..c1b823701 100644 --- a/packages/runtime-core/src/component.ts +++ b/packages/runtime-core/src/component.ts @@ -506,9 +506,16 @@ export interface ComponentInternalInstance { */ asyncResolved: boolean + /** + * effects to be triggered on component activated in keep-alive + * @internal + */ + activatedEffects?: Function[] + // lifecycle isMounted: boolean isUnmounted: boolean + isActivated: boolean isDeactivated: boolean /** * @internal @@ -673,6 +680,7 @@ export function createComponentInstance( // not using enums here because it results in computed properties isMounted: false, isUnmounted: false, + isActivated: true, 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 f2b7bdf97..f3c952bf5 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 = true move(vnode, container, anchor, MoveType.ENTER, parentSuspense) // in case props have changed patch( @@ -149,6 +151,13 @@ const KeepAliveImpl: ComponentOptions = { vnode.slotScopeIds, optimized, ) + + const effects = instance.activatedEffects + if (effects) { + queuePostFlushCb(effects) + instance.activatedEffects!.length = 0 + } + queuePostRenderEffect(() => { instance.isDeactivated = false if (instance.a) { @@ -168,6 +177,7 @@ const KeepAliveImpl: ComponentOptions = { sharedContext.deactivate = (vnode: VNode) => { const instance = vnode.component! + instance.isActivated = false invalidateMount(instance.m) invalidateMount(instance.a) diff --git a/packages/runtime-core/src/renderer.ts b/packages/runtime-core/src/renderer.ts index 022571050..b8cdffc34 100644 --- a/packages/runtime-core/src/renderer.ts +++ b/packages/runtime-core/src/renderer.ts @@ -1431,6 +1431,21 @@ function baseCreateRenderer( } else { let { next, bu, u, parent, vnode } = instance + // skip updates while parent component is deactivated + // but store effects for next activation + const deactivatedParent = locateDeactivatedParent(instance) + if (deactivatedParent) { + ;( + deactivatedParent.activatedEffects || + (deactivatedParent.activatedEffects = []) + ).push(() => { + if (!instance.isUnmounted) { + update() + } + }) + return + } + if (__FEATURE_SUSPENSE__) { const nonHydratedAsyncRoot = locateNonHydratedAsyncRoot(instance) // we are trying to update some async comp before hydration @@ -2571,6 +2586,19 @@ function locateNonHydratedAsyncRoot( } } +function locateDeactivatedParent(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++)