一、先明确核心结论
keep-alive 是 Vue 内置的抽象组件(不渲染真实 DOM ),它的核心作用是缓存被包裹的组件实例,缓存的关键数据结构如下:
- 缓存容器:
keep-alive实例上的this.cache(一个对象,key 是组件的「缓存标识」,value 是组件实例); - 辅助记录:
this.keys(一个数组,存储缓存组件的 key,用于实现max缓存数量限制); - 挂载关系:被缓存的组件实例 → 作为
this.cache对象的属性值 → 挂在keep-alive组件实例上,而非被缓存组件自己的实例上。
二、keep-alive 挂载缓存的完整过程(分步骤拆解)
以 Vue 2 为例(Vue 3 逻辑一致,仅源码实现细节略有差异),核心流程如下:
步骤 1:keep-alive 初始化,创建缓存容器
keep-alive 组件初始化时,会在自身实例上创建两个核心属性,用于存储缓存:
javascript
// keep-alive 组件的初始化逻辑(简化版)
export default {
name: 'keep-alive',
abstract: true, // 抽象组件,不参与DOM渲染
props: {
include: [String, RegExp, Array], // 需缓存的组件
exclude: [String, RegExp, Array], // 排除缓存的组件
max: [String, Number] // 最大缓存数量
},
created() {
this.cache = Object.create(null); // 缓存容器:{ key: 组件实例 }
this.keys = []; // 缓存key列表:[key1, key2...]
},
// ...其他生命周期
}
this.cache:空对象,后续用来存「缓存标识 → 组件实例」的映射;this.keys:空数组,记录缓存 key 的顺序,用于 LRU 淘汰(超出 max 时删除最久未使用的缓存)。
步骤 2:组件首次渲染,判断是否缓存
当 keep-alive 包裹的组件首次渲染时,keep-alive 的 render 函数会执行核心逻辑:
- 获取被包裹组件的**「缓存标识」**(key):
-
- 默认 key:
组件名 + 组件实例的uid(避免同组件不同实例冲突); - 自定义 key:可通过
key属性指定(如<keep-alive><component :is="comp" :key="compKey" /></keep-alive>)。
- 默认 key:
- 判断是否符合缓存规则(
include/exclude):
-
- 若符合:将组件实例存入
this.cache,并把 key 加入this.keys; - 若不符合:不缓存,直接渲染组件(和普通组件一样)。
- 若符合:将组件实例存入
举个例子:
xml
<keep-alive>
<router-view v-if="$route.meta.keepAlive" />
</keep-alive>
步骤 3:缓存组件实例,挂载到 keep-alive 上
核心逻辑简化如下:
kotlin
// keep-alive 的 render 函数核心逻辑(简化版)
render() {
const slot = this.$slots.default;
const vnode = getFirstComponentChild(slot); // 获取被包裹的第一个组件vnode
const componentOptions = vnode && vnode.componentOptions;
if (componentOptions) {
// 1. 生成缓存key(核心:唯一标识组件实例)
const key = this.getCacheKey(vnode);
const { cache, keys } = this;
// 2. 判断是否需要缓存(符合include,不符合exclude)
if (this.shouldCache(componentOptions)) {
// 3. 若缓存中已有该组件实例,直接复用
if (cache[key]) {
vnode.componentInstance = cache[key].componentInstance;
// 更新key顺序(LRU:把当前key移到最后,标记为最近使用)
remove(keys, key);
keys.push(key);
} else {
// 4. 首次渲染:将组件vnode(包含实例)存入缓存
cache[key] = vnode;
keys.push(key);
// 5. 超出max时,删除最久未使用的缓存
if (this.max && keys.length > parseInt(this.max)) {
pruneCacheEntry(cache, keys[0], keys, this._vnode);
}
}
// 标记组件为"被缓存",避免重复初始化
vnode.data.keepAlive = true;
}
}
return vnode;
}
关键挂载动作 :cache[key] = vnode → 组件的 vnode(包含 componentInstance 即组件实例)被作为 cache 对象的属性值,挂载到 keep-alive 实例的 this.cache 上。
步骤 4:组件再次渲染,复用缓存实例
当被缓存的组件需要再次渲染时(比如路由切换后返回):
keep-alive从this.cache中根据 key 取出对应的组件实例;- 将缓存的实例赋值给新的 vnode 的
componentInstance; - 直接复用该实例渲染,不再执行组件的
created/mounted等生命周期(而是触发activated钩子)。
三、关键细节:为什么缓存不挂在被缓存组件自己的实例上?
- 逻辑合理性 :
keep-alive是「缓存管理者」,应该由管理者统一存储和管理所有被缓存的组件,而非让组件自己存储; - 避免内存泄漏 :若缓存挂在被缓存组件实例上,组件实例本身无法被销毁(因为缓存引用了它),而
keep-alive统一管理可通过max、exclude主动清理缓存; - 多实例隔离 :多个
keep-alive组件的缓存是隔离的(比如页面 A 和页面 B 各有一个keep-alive),每个keep-alive实例有自己的cache,不会互相干扰。
总结:
一、先肯定你的正确认知
- ✅
keep-alive是 Vue 官方内置的抽象组件(不渲染真实 DOM),被包裹的组件实例 / 状态会挂载在keep-alive实例的cache对象上(而非组件自身); - ✅ 缓存的核心内容是组件的 VNode (包含组件实例、DOM 节点描述、数据状态如
data/props/ 输入框值等); - ✅ 组件的缓存触发和「组件失活」强相关(比如路由跳转导致组件被隐藏)。
二、需要修正 / 补充的关键细节
细节 1:"组件经过 keep-alive 时被缓存" → 不是 "经过",而是 "组件被 keep-alive 包裹且失活时才缓存"
keep-alive 不会 "主动拦截" 组件,而是当被包裹的组件从「激活状态」变为「失活状态」时,才会将其 VNode 存入缓存(而非加载时就缓存)。
- 激活状态:组件在页面中可见(比如当前路由匹配的 Home 组件);
- 失活状态:组件被隐藏(比如跳转到 List 路由,Home 组件被
router-view卸载)。
简单说:keep-alive 是 "挽留" 即将被销毁的组件 ------ 默认情况下,组件失活会被销毁,而 keep-alive会把它存入缓存,避免销毁。
细节 2:"只有页面跳转才缓存" → 不完全对,「组件失活」都触发缓存(不止路由跳转)
路由跳转是最常见的 "组件失活" 场景,但不是唯一场景:
- 场景 1(路由跳转):
/home→/list,Home 组件失活 → 被缓存; - 场景 2(条件渲染):
<keep-alive><component :is="compName" /></keep-alive>,当compName从Home改为List时,Home 失活 → 被缓存; - 场景 3(v-if 隐藏):
<keep-alive><div v-if="show">Home组件</div></keep-alive>,当show从true改为false时,Home 失活 → 被缓存。
核心:只要 keep-alive 包裹的组件从 "渲染在页面上" 变为 "不渲染",且符合 include/exclude 规则,就会被缓存。
细节 3:"只加载页面不跳转,不会缓存" → 准确说:"组件未失活,缓存容器中已有该组件的 VNode,但未触发「缓存复用」"
即使不跳转,只要组件被 keep-alive 包裹并完成首次渲染:
keep-alive的cache中 已经存入了该组件的 VNode (可以通过前面的代码查到);- 只是因为组件未失活,所以不会触发
activated钩子,也不会体现出 "缓存效果"(比如输入框输入内容,不跳转的话,内容本来就在,看不出缓存); - 只有当组件失活后再次激活(比如跳转回来),才会从缓存中复用 VNode,此时能看到 "状态保留"(比如输入框内容还在)------ 这才是缓存的 "可见效果"。
举个直观例子:
- 步骤 1:访问
/home,Home 组件渲染(激活),keep-alive.cache中已有 Home 的 VNode(但未体现缓存); - 步骤 2:在 Home 输入框输入 "123",跳转到
/list(Home 失活),keep-alive保留 Home 的 VNode(包含输入框的 "123"); - 步骤 3:跳回
/home(Home 激活),keep-alive复用缓存的 VNode,输入框仍显示 "123"------ 这就是缓存的效果。
如果只停留在步骤 1(不跳转),虽然缓存容器中有 Home 的 VNode,但因为没有 "失活→激活" 的过程,所以看不到缓存的效果,并非 "没有缓存"。
细节 4:缓存的 VNode 包含什么?→ 不止节点 / 属性,还有组件的「完整实例状态」
VNode 是组件的 "虚拟描述",缓存 VNode 本质是缓存组件实例:
- 包含 DOM 结构描述 (比如
<div class="home">); - 包含组件的响应式数据 (
data/computed/props); - 包含组件的 DOM 状态(输入框值、滚动条位置、复选框勾选状态);
- 包含组件的生命周期状态 (不会再执行
created/mounted,而是执行activated)。
三、总结:精准理解 keep-alive 的缓存逻辑
- 挂载关系 :
keep-alive是 "缓存管理者",被包裹组件的 VNode(含实例 / 状态)挂载在keep-alive实例的cache对象上; - 缓存触发 :组件被
keep-alive包裹 + 组件从「激活→失活」(路由跳转 / 条件隐藏等)→ 存入缓存; - 缓存复用 :组件从「失活→激活」→ 从
cache中取出 VNode 复用(不重新创建实例,保留状态); - 可见效果:只有 "失活→激活" 的过程,才能体现缓存(状态保留),仅加载组件不跳转,缓存存在但无 "可见效果"。
简单记: keep-alive 的核心是 "保活"------ 不让失活的组件销毁,而是存入缓存,下次激活时直接复用,避免重复创建 / 销毁,同时保留组件状态。
keep-alive组件加载生命周期对比
| 阶段 | Vue 2 生命周期 | Vue 3 组合式 API | 核心特点 |
|---|---|---|---|
| 首次加载 | beforeCreate → created → beforeMount → mounted → activated | setup → onBeforeMount → onMounted → onActivated | 完整生命周期,最后触发激活钩子 |
| 失活缓存 | deactivated | onDeactivated | 仅触发失活钩子,不销毁组件 |
| 二次加载 | activated | onActivated | 仅触发激活钩子,跳过创建 / 挂载 |
| 缓存销毁 | deactivated → beforeDestroy → destroyed | onDeactivated → onBeforeUnmount → onUnmounted | 先失活,再销毁 |