Vue 中 KeepAlive 的深入理解和使用
原文主要介绍 Vue 2 的
<keep-alive>。本文保留原文关于组件缓存、include、exclude、max、路由和activated/deactivated的内容,并补充 Vue 3.5 与 Vue Router 4 的写法。
一、什么是 KeepAlive?
KeepAlive 是 Vue 内置组件,用于缓存动态组件的组件实例。当组件从当前视图切换出去时,Vue 可以把它停用而不是立即卸载;下次切换回来时复用原来的实例和状态。
<KeepAlive>
<component :is="currentComponent" />
</KeepAlive>
缓存的是组件实例及其子树的运行状态,不是把所有 DOM 永久固定在页面上。被缓存的组件在非活动期间通常会从 DOM 树中移出,但不会执行完整的卸载流程。
因此要区分:
- 第一次显示:仍然会创建组件并执行
mounted; - 切换离开:执行
deactivated,组件实例保留在缓存中; - 再次显示:执行
activated,不会重新执行一次mounted; - 父级卸载、缓存被淘汰或不再满足缓存条件:组件才会真正卸载,执行
unmounted(Vue 3)或destroyed(Vue 2)。
二、基本用法
Vue 3 动态组件
<script setup>
import { ref } from 'vue'
import HomePanel from './HomePanel.vue'
import ProfilePanel from './ProfilePanel.vue'
const currentComponent = ref(HomePanel)
</script>
<template>
<button @click="currentComponent = HomePanel">首页</button>
<button @click="currentComponent = ProfilePanel">个人信息</button>
<KeepAlive>
<component :is="currentComponent" />
</KeepAlive>
</template>
如果没有 KeepAlive,切换动态组件通常会卸载旧组件并创建新组件;加入 KeepAlive 后,组件内部输入框、滚动位置和局部状态可以被保留。
Vue 2 的写法
<keep-alive>
<component :is="currentTabComponent" />
</keep-alive>
Vue 2 中组件名一般来自组件的 name 选项或注册名称;Vue 3 中 include/exclude 也主要依据组件的 name 匹配。Vue 3.2+ 使用 <script setup> 时,组件名通常可以根据文件名推断;需要精确控制时可显式声明:
<script setup>
defineOptions({
name: 'ProfilePanel',
})
</script>
defineOptions 是 Vue 3.3+ 的编译器宏;Vue 2 应继续使用:
export default {
name: 'ProfilePanel',
}
三、activated 和 deactivated
被 KeepAlive 包裹的有状态组件可以使用两组生命周期:
Options API
export default {
activated() {
// 再次进入缓存组件时执行,也可在第一次激活时执行
this.refreshIfNeeded()
},
deactivated() {
// 暂时离开页面但仍留在缓存中
this.pausePolling()
},
}
Composition API
<script setup>
import { onActivated, onDeactivated } from 'vue'
onActivated(() => {
console.log('组件被激活')
})
onDeactivated(() => {
console.log('组件被停用,但可能仍在缓存中')
})
</script>
deactivated 适合暂停轮询、停止动画、移除临时监听;activated 适合重新检查数据或恢复轮询。全局事件监听和定时器仍然要在 onUnmounted 中做最终清理,因为组件可能长期留在缓存中,甚至直到父组件销毁才卸载。
<script setup>
import {
onActivated,
onDeactivated,
onUnmounted,
} from 'vue'
let timer
function startPolling() {
if (timer == null) {
timer = window.setInterval(() => {
console.log('poll')
}, 5000)
}
}
function stopPolling() {
if (timer != null) {
window.clearInterval(timer)
timer = undefined
}
}
onActivated(startPolling)
onDeactivated(stopPolling)
onUnmounted(stopPolling)
</script>
四、include、exclude 和 max
KeepAlive 常用的三个 props:
include:只有名称匹配的组件才缓存;exclude:名称匹配的组件不缓存;max:最多缓存的组件实例数量,超出时淘汰最久未使用的实例。
它们支持字符串、正则表达式或数组。绑定数组和正则表达式时必须使用 v-bind:
<KeepAlive
:include="['HomePanel', 'ProfilePanel']"
exclude="ExpensivePreview"
:max="5"
>
<component :is="currentComponent" />
</KeepAlive>
字符串也可以使用逗号分隔的组件名:
<KeepAlive include="HomePanel,ProfilePanel">
<component :is="currentComponent" />
</KeepAlive>
当前实现可以概括为:组件名称需要满足 include(如果存在),同时不能命中 exclude:
matches(include, name) && !matches(exclude, name)
所以当同一个组件同时匹配 include 和 exclude 时,不会被缓存,exclude 的排除效果优先。
<KeepAlive include="HomePanel,ProfilePanel" exclude="ProfilePanel">
<component :is="currentComponent" />
</KeepAlive>
max 使用近似 LRU(最近最少使用)策略管理缓存。达到上限时,最久没有被访问的缓存实例会被移除;被移除的实例会进入真正的卸载流程,而不是只触发一次 deactivated。
五、配合 Vue Router 使用
Vue Router 3 的历史写法
原文中的直接包裹方式在 Vue 2/Vue Router 3 项目中仍可能看到:
<keep-alive>
<router-view />
</keep-alive>
它会尝试缓存所有匹配到的路由组件。若只缓存部分组件,可以通过 include/exclude 或路由组件名称控制。
Vue Router 4 的推荐写法
Vue Router 4 推荐使用 router-view 的插槽拿到实际的路由组件,再决定是否包裹:
<template>
<RouterView v-slot="{ Component }">
<KeepAlive :include="['HomePage', 'ProfilePage']">
<component :is="Component" />
</KeepAlive>
</RouterView>
</template>
KeepAlive 匹配的是实际路由组件的 name,不是路由的 name 字段。路由配置中的 name 和组件的 name 是两个不同概念。
如果使用路由 meta 控制是否缓存,可以写成:
<template>
<RouterView v-slot="{ Component, route }">
<KeepAlive v-if="route.meta.keepAlive">
<component :is="Component" />
</KeepAlive>
<component v-else :is="Component" />
</RouterView>
</template>
路由配置:
import type { RouteRecordRaw } from 'vue-router'
export const routes: RouteRecordRaw[] = [
{
path: '/',
name: 'home',
component: () => import('./views/HomePage.vue'),
meta: { keepAlive: true },
},
{
path: '/profile',
name: 'profile',
component: () => import('./views/ProfilePage.vue'),
meta: { keepAlive: false },
},
]
如果同一个组件需要根据不同参数保留不同实例,可以给动态组件设置稳定且有区分度的 key:
<RouterView v-slot="{ Component, route }">
<KeepAlive>
<component :is="Component" :key="route.fullPath" />
</KeepAlive>
</RouterView>
是否需要 key 取决于业务:不设置 key 可能复用同一组件类型的实例;设置 fullPath 会把不同参数/查询条件视为不同缓存项,也会增加缓存数量。
六、常见误区和防坑指南
1. KeepAlive 不会让组件永远 mounted
“被缓存的组件不会 mounted”是错误的。第一次创建并挂载时仍然会执行 mounted;之后从缓存中恢复主要执行 activated。
2. deactivated 不等于销毁
组件离开当前视图时,可能只是被停用。不要在 deactivated 中假设组件已经销毁,也不要只在这里做一次性永久资源清理;最终资源释放应放在 onUnmounted/unmounted。
3. 组件必须有可匹配的 name
include/exclude 依赖组件 name。Vue 2 可以使用 name 选项或注册名;Vue 3 <script setup> 可以使用文件名推断,也可以使用 defineOptions({ name }) 显式设置。
4. include 和 exclude 使用正则、数组时要绑定
<KeepAlive :include="/^(Home|Profile)/" />
<KeepAlive :include="['HomePage', 'ProfilePage']" />
不绑定时,数组或正则表达式可能被当成普通字符串处理。
5. 函数组件通常没有需要缓存的组件实例状态
KeepAlive 的价值在于缓存有状态组件实例。无状态函数式组件没有同样的实例生命周期,不应把它当作主要缓存对象。
6. 缓存不是数据同步方案
被缓存组件中的数据不会自动因为路由进入而重新请求。需要刷新时可以在 onActivated 中调用请求,也可以由 Pinia、query cache 或父组件统一管理数据。
7. 不要用 $destroy 清理 Vue 3 缓存
Vue 2 项目可能使用 $destroy 或修改内部缓存;Vue 3 已移除 $destroy,也没有推荐的公开 API 让业务直接操作 KeepAlive 内部 cache。应通过改变 include、切换 key 或调整组件结构实现明确的缓存策略。
8. Teleport 与 KeepAlive 不是一回事
KeepAlive 缓存组件实例,Teleport 改变 DOM 插入位置。弹窗既可以被 KeepAlive 缓存,也可以使用 Teleport 放到 body,但两者解决的是不同问题。
七、总结
KeepAlive缓存动态组件实例,使组件离开视图时停用而不是立即卸载。- 第一次显示仍会执行 mounted;重新进入执行 activated;真正淘汰或父级卸载时才执行 unmounted/destroyed。
include、exclude按组件 name 匹配,exclude对同时命中的组件具有排除效果。max会限制缓存数量并淘汰最久未使用的实例。- Vue Router 4 推荐通过
RouterView插槽包裹实际的Component。 - 需要暂停/恢复任务时使用
onActivated/onDeactivated,最终清理仍放在onUnmounted。
官方参考
- Vue 3 KeepAlive
- Vue 3 内置组件 API
- Vue 3 生命周期
- Vue 2 动态组件与 KeepAlive
- Vue Router 4 RouterView 插槽
- Vue Router 4 路由 meta 字段
原文出处
作者:Junyang
原文链接:https://juejin.cn/post/6844903918313406472
来源:稀土掘金。著作权归作者所有,商业转载请联系作者获得授权,非商业转载请注明出处。