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 11 commits into
base: main
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from 9 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)))
yangxiuxiu1115 marked this conversation as resolved.
Show resolved Hide resolved
},
}

const App = createApp({
setup() {
return () => {
return [
h(KeepAlive, null, [
yangxiuxiu1115 marked this conversation as resolved.
Show resolved Hide resolved
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

keepAliveEffct: 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,

keepAliveEffct: [],
yangxiuxiu1115 marked this conversation as resolved.
Show resolved Hide resolved

// lifecycle hooks
// not using enums here because it results in computed properties
isMounted: false,
isUnmounted: false,
isDeactive: false,
yangxiuxiu1115 marked this conversation as resolved.
Show resolved Hide resolved
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.keepAliveEffct
queuePostFlushCb(effects)
instance.keepAliveEffct.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 = locateDeactiveKeeyAlive(instance)
if (keepAliveParent) {
keepAliveParent.keepAliveEffct.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 locateDeactiveKeeyAlive(instance: ComponentInternalInstance | null) {
yangxiuxiu1115 marked this conversation as resolved.
Show resolved Hide resolved
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