事情是这样的——因为团队人手紧张,我临时接手了一个三年前的老项目,技术栈是 Vue2。需求提了个看似简单的需求:“我们这个部分页面能不能加个缓存?别每次切换回来都重新加载了。”
我心想:“这年头了,Vue 的缓存不是一句 keep-alive
就搞定?”
于是信手一加,页面状态完美保留,效果看起来相当不错。我刚准备喝口茶,结果测试同学一句话把我拉了回来:
“咦?你这个 tab 不是关掉了吗?怎么再点回来状态还在?这缓存清不了啊!”
我:“啊这……”
一开始我以为是 key 写错了,后来排查路由、组件、Vuex,越查越不对劲。原来,Vue2 的缓存机制可不是“加了就有”,关键在于:你还得“能控”。
就这样,一个简单的 keep-alive
,让我卷进了缓存的世界,开始了一场追踪组件生命周期、解构 include 名称匹配、调和业务 tab 与缓存机制之间矛盾的技术旅程。
所以这篇文章,不仅是为了实现“缓存生效”,更重要的是:
如何优雅控制缓存何时启用、何时销毁?Vue2 的缓存机制有哪些坑?Vue3 又做了哪些改进?在 tab 标签页场景下,缓存策略到底该怎么设计?
别急,马上我们就来一探究竟 ——走进 Vue 的缓存世界,重新认识你以为“已经懂了”的 keep-alive
。
当你接到“做个页面缓存”的需求,第一个想到的当然就是给 router-view
包个
:
我们锁定它的位置:在 layout/components/AppMain.vue
。没错,它就藏在这里。
这个写法确实能让页面缓存起来,状态也保得住,看上去一切正常,但——你关掉 tab,再点回来,它还在,页面状态一点没变,缓存“死死不放手”。
聪明的小伙伴马上会发现问题:它的缓存机制只依赖于路由的 meta.keepAlive
,只要路由说“我需要缓存”,那这个组件就一直活在内存里,除非你手动刷新整个页面或关掉浏览器。
于是我开始溯源,试图找到这个实现的“前身”。看着目录结构、组件命名方式,猛地一拍大腿:
“这不是 Vue-Element-Admin 的路子吗?”
果然,扒一扒它的源代码,发现了真正的“缓存控制中枢”:
在它的 AppMain.vue
中,
的 include
并不是直接根据路由,而是通过 Vuex 中的 cachedViews
动态控制:
你看人家就做得很细:
那么问题来了:
为什么你接手的项目,从“原版 Vue-Element-Admin”改成了只用
meta.keepAlive
?
是出于简化结构?还是历史遗留?没人知道。只知道这个“轻松写法”现在让你头大。
在继续之前,我们要从“缓存机制的本质”入手,重新梳理:
keep-alive
到底缓存谁、怎么缓存?接下来,我们将进入Vue2 的
是怎么工作的,从底层机制开始抽丝剥茧,构建我们自己的“缓存掌控术”。
你以为
就是个“组件容器”?
不,它是个内存缓存管理器。你一不小心,它就把你的页面塞进了“长期保存区”,永不销毁——这不是玄学,是机制。
Vue 的
是个抽象组件,它不会渲染 DOM,它的唯一任务是:
把被包裹的组件实例缓存到内存中,并在需要时恢复它。
一旦组件被缓存,它的生命周期就不再完整执行,而是触发特定的“复活钩子”:
生命周期钩子 | 说明 |
---|---|
activated |
组件从缓存中被激活(再次显示) |
deactivated |
组件被缓存但不是销毁(暂时隐藏) |
这一步是重头戏,Vue2 是通过组件的 name
与
进行匹配的:
组件自身必须定义 name
,否则无法被识别:
export default {
name: 'UserList'
}
⛔️ 坑点:如果 name 缺失 or 写错,缓存直接失效,还不报错!
meta.keepAlive
起什么作用?它其实是我们开发者约定的“提示标签”:
{
path: '/user/list',
component: UserList,
meta: { keepAlive: true }
}
Vue 本身不认识这个字段,是你在 AppMain.vue
或其他地方通过它做判断:
说白了:路由 meta 就是决定“要不要包
”,但真正决定“能不能缓存”的,是 name 和 include。
真正的掌控来自这里:
而 cachedViews
可能来自 Vuex:
computed: {
cachedViews() {
return this.$store.state.tagsView.cachedViews
}
}
每打开一个标签页,就向其中 push
对应组件的 name
;每关闭标签页,就 remove
对应 name,这样缓存组件自动增删。
Vue 会默认缓存所有曾经加载过的组件,直到页面刷新、或浏览器进程结束。这就是你遇到“缓存死活清不掉”的真正原因!
条件 | 是否必须 | 作用 |
---|---|---|
组件有 name |
✅ 是 | 提供唯一标识 |
使用
|
✅ 是 | 启用缓存机制 |
include 正确匹配 | 推荐 | 控制缓存清单 |
meta.keepAlive | ❌ 非必须 | 仅用于条件判断 |
避免缓存错乱或异常,可以使用更细粒度的 key
:
$route.path
只看路径,不包含参数;$route.fullPath
更细致,含 query 等,可区分多个同路径不同参数的页面。下一节,我们将动手实现:用 Vuex + keep-alive 构建你的缓存调度中心
我们会:
cachedViews
;将是整个 keep-alive 篇的核心,我们不再纸上谈兵,而是撸起袖子实现一个动态可控的缓存机制,告别“缓存失控、页面重载、状态丢失”三连崩溃。
我们的目标是实现如下功能:
每打开一个路由 tab,就缓存对应组件
每关闭一个 tab,就移除缓存
支持多标签页切换不刷新
支持手动刷新页面(重载组件)
所有缓存逻辑集中管理,易扩展、可追踪
核心关键词:Vuex、cachedViews、组件 name、keep-alive include
src/
├── layout/
│ └── components/
│ └── AppMain.vue # 路由出口 + 缓存处理
├── store/
│ └── modules/
│ └── tagsView.js # tab 管理 + 缓存列表
├── router/
│ └── index.js # 设置 meta.keepAlive
├── views/
│ └── YourPage.vue # 必须定义 name!
name
// views/UserList.vue
export default {
name: 'UserList',
...
}
否则 keep-alive
根本识别不了该组件!
// store/modules/tagsView.js
const state = {
cachedViews: []
}
const mutations = {
ADD_CACHED_VIEW(state, view) {
if (state.cachedViews.includes(view.name)) return
state.cachedViews.push(view.name)
},
DEL_CACHED_VIEW(state, view) {
const index = state.cachedViews.indexOf(view.name)
if (index > -1) state.cachedViews.splice(index, 1)
},
CLEAR_CACHED_VIEWS(state) {
state.cachedViews = []
}
}
export default {
namespaced: true,
state,
mutations
}
tagsView
标签页栏)// 打开标签页时
store.commit('tagsView/ADD_CACHED_VIEW', {
name: to.matched[0].components.default.name
})
// 关闭标签页时
store.commit('tagsView/DEL_CACHED_VIEW', {
name: closedTab.name
})
重点说明:
:include="cachedViews"
来控制缓存组件;:key="$route.fullPath"
让 Vue 判断页面是否需要重渲;keepAlive: true
和 false 的路由逻辑。methods: {
refreshView(view) {
this.$store.commit('tagsView/DEL_CACHED_VIEW', view)
this.$nextTick(() => {
this.$router.replace({ path: '/redirect' + view.fullPath }) // redirect 路由统一刷新
})
}
}
组件 | 功能 |
---|---|
name |
缓存组件必须声明 |
meta.keepAlive |
判断是否需要缓存 |
Vuex cachedViews |
管理缓存白名单 |
keep-alive :include |
控制缓存行为 |
key="$route.fullPath" |
区分路由组件状态 |
下一节我们将对比:Vue3 的缓存机制到底优化了哪些点?是不是更“智能”、更“好用”?
我们将从原理差异到开发实践,对比 Vue2 与 Vue3 的缓存实现差异,揭示 Vue3 对痛点的改良,也为后续的升级迁移打下基础。
先来一张总结图:
对比项 | Vue2 | Vue3 |
---|---|---|
使用方式 |
|
(PascalCase 推荐) |
匹配依据 | component.name |
同样依赖 name ,但增强支持动态组件 |
生命周期 | activated / deactivated |
同名钩子,支持 Composition API |
缓存控制 | include/exclude |
同样支持,但更加灵活 |
类型支持 | 较弱 | 强类型(TS 支持更友好) |
动态缓存更新 | 手动维护 | 支持组合式响应式控制 |
缓存粒度 | 组件级 | 同样组件级,未来配合 signal 有望更细粒度 |
一句话总结:Vue3 并没有“重写” keep-alive,但它带来了更强的组合式可控性,真正掌控缓存行为不再是 hack,而是内建能力。
Vue2 的生命周期钩子:
export default {
activated() {
console.log('组件被激活')
},
deactivated() {
console.log('组件被缓存')
}
}
Vue3 完全支持上述写法,同时新增组合式 API 支持:
import { onActivated, onDeactivated } from 'vue'
setup() {
onActivated(() => {
console.log('组件被激活')
})
onDeactivated(() => {
console.log('组件被缓存')
})
}
优势:逻辑归位、状态可组合、逻辑可抽离(自定义 hooks)
Vue3 的 KeepAlive
支持绑定一个响应式的数组作为 include
,动态增删更自然:
import { ref } from 'vue'
const cachedNames = ref(['Dashboard', 'Settings'])
function addToCache(name) {
if (!cachedNames.value.includes(name)) {
cachedNames.value.push(name)
}
}
function removeFromCache(name) {
cachedNames.value = cachedNames.value.filter(n => n !== name)
}
Vue3 的响应式缓存列表结合 Composition API,可以封装为
useCacheManager()
composable,彻底告别 Vuex 操作。
Vue3 修复了一个 Vue2 的老问题:
在 Vue2 中,匿名组件或者 setup 返回的匿名组件可能无法被缓存。
Vue3 明确要求组件传入 name
,并可以配合 自动生效:
极大提升了缓存准确率,尤其在 SFC 单文件组件中。
虽然不直接 related,但 Vue3 的组合能力为缓存机制开辟了新空间:
KeepAlive
,避免加载抖动;Teleport
结合保持 modal/cache 状态;特性 | Vue2 | Vue3 |
---|---|---|
缓存控制方式 | include/exclude | include/exclude(响应式增强) |
生命周期钩子 | activated / deactivated | 同名 + 组合式支持 |
组件识别方式 | name | name(setup 支持更好) |
动态缓存列表 | Vuex 手动维护 | 可组合响应式维护 |
支持 async setup | ❌ | ✅ |
更高扩展能力 | ⛔️ | ✅ 支持 Teleport/Suspense 等周边结合 |