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 10 commits into
base: main
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
46 changes: 46 additions & 0 deletions packages/runtime-core/__tests__/components/KeepAlive.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ import {
type TestElement,
cloneVNode,
createApp,
createVNode,
defineAsyncComponent,
defineComponent,
h,
Expand All @@ -22,6 +23,7 @@ import {
reactive,
ref,
render,
resolveDynamicComponent,
serializeInner,
shallowRef,
} from '@vue/runtime-test'
Expand Down Expand Up @@ -1173,4 +1175,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(createVNode(resolveDynamicComponent(activeView.value)))
},
}

const App = createApp({
setup() {
return () => {
return [
h(KeepAlive, null, [
createVNode(resolveDynamicComponent(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
isDeactive: 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,
isDeactive: 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.isDeactive = 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.isDeactive = 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.isDeactive) {
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