Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

fix(keep-alive): avoid duplicate mounts of deactivate components #12042

Open
wants to merge 12 commits into
base: main
Choose a base branch
from
44 changes: 44 additions & 0 deletions packages/runtime-core/__tests__/components/KeepAlive.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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, [
yangxiuxiu1115 marked this conversation as resolved.
Show resolved Hide resolved
h(HomeView, {
key: activeView.value.name,
}),
]),
]
}
},
})
App.mount(nodeOps.createElement('div'))
expect(mountedHome).toHaveBeenCalledTimes(0)
activeView.value = Home
await nextTick()
expect(mountedHome).toHaveBeenCalledTimes(1)
})
})
6 changes: 6 additions & 0 deletions packages/runtime-core/src/component.ts
Original file line number Diff line number Diff line change
Expand Up @@ -506,9 +506,12 @@ export interface ComponentInternalInstance {
*/
asyncResolved: boolean

keepAliveEffect: Function[]

// lifecycle
isMounted: boolean
isUnmounted: boolean
isActivated: boolean
isDeactivated: boolean
/**
* @internal
Expand Down Expand Up @@ -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,
Expand Down
8 changes: 8 additions & 0 deletions packages/runtime-core/src/components/KeepAlive.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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)[]

Expand Down Expand Up @@ -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(
Expand All @@ -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) {
Expand All @@ -168,6 +175,7 @@ const KeepAliveImpl: ComponentOptions = {

sharedContext.deactivate = (vnode: VNode) => {
const instance = vnode.component!
instance.isActivated = true
invalidateMount(instance.m)
invalidateMount(instance.a)

Expand Down
23 changes: 23 additions & 0 deletions packages/runtime-core/src/renderer.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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++)
Expand Down