This commit is contained in:
yangxiuxiu 2025-05-05 20:38:32 +00:00 committed by GitHub
commit 8e68075976
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
4 changed files with 90 additions and 0 deletions

View File

@ -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)
})
})

View File

@ -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,

View File

@ -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)

View File

@ -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++)