在 Vue 开发中,你是否遇到过:
"切换页面时,列表滚动位置丢失?"
"组件反复创建销毁,性能卡顿?"
"表单输入内容在路由切换后清空?"
keep-alive
就是解决这些问题的"银弹"。
它能缓存组件状态,避免重复渲染,大幅提升用户体验。
但你是否真正理解:
keep-alive
缓存的是什么?- 它是如何实现的?
- LRU 策略如何工作?
本文将从 源码级 深度解析 keep-alive
的核心机制。
一、keep-alive
是什么?
✅ 核心定义
keep-alive
是 Vue 的内置抽象组件,用于:
缓存动态组件或路由视图的实例,避免重复销毁与重建。
📌 基础用法
vue
<!-- 缓存所有匹配的组件 -->
<keep-alive>
<component :is="currentView"></component>
</keep-alive>
<!-- 或缓存路由视图 -->
<keep-alive>
<router-view></router-view>
</keep-alive>
二、三大核心属性
属性 | 类型 | 说明 |
---|---|---|
include |
`string | RegExp |
exclude |
`string | RegExp |
max |
`number | string` |
💡 实战示例
vue
<keep-alive
include="UserInfo,OrderList"
exclude="SearchResult"
:max="10"
>
<router-view />
</keep-alive>
- ✅ 缓存
UserInfo
和OrderList
; - ❌ 不缓存
SearchResult
; - ⚠️ 最多缓存 10 个实例,超出时按 LRU 策略清除。
三、keep-alive
如何工作?源码级解析
🧩 核心数据结构
js
// 初始化缓存
created() {
this.cache = Object.create(null); // 存储 { key: vnode }
this.keys = []; // 存储 key,实现 LRU
}
cache
: 缓存对象,key
→vnode
映射;keys
: key 数组,记录访问顺序,实现 LRU(最近最少使用) 策略。
🔁 render
函数核心流程
步骤 1:获取第一个子组件
js
const slot = this.$slots.default;
const vnode = getFirstComponentChild(slot);
💡
keep-alive
只对第一个子组件生效。
步骤 2:判断是否需要缓存
js
const name = getComponentName(vnode.componentOptions);
// 不在 include 或在 exclude 中,直接返回
if (
(include && !matches(include, name)) ||
(exclude && matches(exclude, name))
) {
return vnode;
}
步骤 3:生成唯一 key
js
const key = vnode.key == null
? componentOptions.Ctor.cid + (componentOptions.tag ? `::${componentOptions.tag}` : '')
: vnode.key;
❗ 为什么不用
cid
唯一标识?因为同一个构造函数可以注册为多个本地组件 ,
cid
相同但组件不同。
步骤 4:缓存命中与 LRU 更新
js
if (cache[key]) {
// 命中缓存
vnode.componentInstance = cache[key].componentInstance;
// LRU:将 key 移到末尾(最新使用)
remove(keys, key);
keys.push(key);
} else {
// 未命中,加入缓存
cache[key] = vnode;
keys.push(key);
// 超出 max,清除最久未使用的
if (max && keys.length > parseInt(max)) {
pruneCacheEntry(cache, keys[0], keys);
}
}
步骤 5:标记 keepAlive
js
vnode.data.keepAlive = true;
这个标记告诉 Vue:跳过组件的
created
、mounted
等生命周期。
四、缓存的是什么?真相揭秘
✅ 正确答案
keep-alive
缓存的是:
组件的
vnode
实例 和componentInstance
(组件实例)
包括:
- 组件的 DOM 结构;
- 组件的
data
状态; - 事件监听器;
- 子组件树。
❌ 常见误解
- ❌ 不是只缓存 HTML 字符串;
- ❌ 不是只缓存
data
对象; - ❌ 不是"冻结"组件。
五、生命周期钩子:activated
与 deactivated
当组件被 keep-alive
包裹时,会新增两个生命周期钩子:
钩子 | 触发时机 |
---|---|
activated |
组件被激活(从缓存中显示)时调用 |
deactivated |
组件被缓存(切换离开)时调用 |
💡 实战应用
js
export default {
data() {
return {
timer: null
};
},
activated() {
// 页面可见时恢复轮询
this.timer = setInterval(() => {
console.log('轮询中...');
}, 1000);
},
deactivated() {
// 页面隐藏时清除轮询,避免内存泄漏
clearInterval(this.timer);
}
}
六、首次渲染 vs 缓存渲染
📌 首次渲染
js
init(vnode) {
// 初次渲染,componentInstance 为 undefined
const child = createComponentInstanceForVnode(vnode);
child.$mount();
}
- 正常执行
beforeCreate
→created
→mounted
。
📌 缓存渲染
js
init(vnode) {
if (vnode.componentInstance && vnode.data.keepAlive) {
// 直接复用实例,执行 prepatch
componentVNodeHooks.prepatch(vnode, vnode);
}
}
- 跳过
created
、mounted
; - 直接将缓存的 DOM 插入文档;
- 触发
activated
钩子。
七、LRU 缓存策略详解
📌 LRU(Least Recently Used)
"最近使用的,将来最可能再次使用。"
🔁 工作机制
操作 | 行为 |
---|---|
新组件 | 插入 keys 数组末尾 |
命中缓存 | 将 key 从原位置移除,插入末尾 |
超出 max | 移除 keys[0] (最久未使用) |
📊 示例:max=3
text
操作: A → B → C → A → D
keys: [A] → [A,B] → [A,B,C] → [B,C,A] → [C,A,D]
- D 加入时,B 被清除(最久未使用)。
八、最佳实践与避坑指南
✅ 最佳实践
- 合理设置
max
:避免内存泄漏; - 在
deactivated
中清理资源:定时器、事件监听; - 使用
include
精准控制:避免缓存重型组件。
❌ 常见错误
- 忘记给组件命名(
name
选项); - 在
created
中启动轮询,未在deactivated
中清除; - 缓存大量列表组件,导致内存飙升。
💡 结语
"
keep-alive
是性能与体验的平衡艺术。"
要点 | 说明 |
---|---|
缓存内容 | vnode + 组件实例 |
核心机制 | cache + keys + LRU |
生命周期 | activated / deactivated |
适用场景 | 频繁切换的路由、表单、列表 |
掌握 keep-alive
,你就能:
✅ 显著提升应用流畅度;
✅ 保留用户交互状态;
✅ 优化关键路径性能。