一、核心结论
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 组件初始化时,会在自身实例上创建两个核心属性,用于存储缓存:
// 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; - 若不符合:不缓存,直接渲染组件(和普通组件一样)。
- 若符合:将组件实例存入
举个例子(通过路由属性中的keepAlive进行条件性的判断展示):
<keep-alive>
<router-view v-if="$route.meta.keepAlive" />
</keep-alive>
步骤 3:缓存组件实例,挂载到 keep-alive 上
核心逻辑简化如下:
// 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: 页面数据变化时,keep-alive 会重新缓存吗?
核心结论:不会「重新缓存」,但缓存的组件实例会同步数据变化
keep-alive 缓存的是组件的完整实例(包含响应式数据),而非 “页面加载时的静态快照”。也就是说:
- 组件首次失活存入缓存后,实例的响应式数据依然是 “活的”—— 页面上修改数据(比如输入框、点击修改
data),会直接同步到缓存中的实例,无需「重新缓存」; - 只有 “组件销毁后重新创建” 才会触发新的缓存流程,而数据变化不会导致组件销毁 / 重建,因此不会触发重新缓存。
直观示例验证:
<!-- Home.vue(被 keep-alive 包裹) -->
<script>
export default {
name: 'Home',
data() {
return {
count: 0 // 响应式数据
};
},
activated() {
console.log('缓存激活,当前count:', this.count);
}
};
</script>
<template>
<div>
<p>count: {{ count }}</p>
<button @click="count++">点击加1</button>
</div>
</template>
操作步骤 & 结果:
- 首次访问
/home:count=0,组件存入缓存(cache中保存 count=0 的实例); - 点击按钮,count=1(数据变化,缓存中的实例 count 同步变为 1,无重新缓存);
- 跳转到
/list(Home 失活):缓存保留的是 count=1 的实例(最新状态); - 跳回
/home(二次加载):直接复用缓存实例,count 仍为 1,activated打印 1。
关键原理:
- 缓存的
VNode关联的是组件的响应式实例,数据是双向绑定的 —— 页面上修改数据,缓存实例的属性会同步更新; keep-alive只有 “首次存入缓存” 和 “淘汰缓存” 两个关键动作,数据变化不会触发缓存的增 / 删 / 改,只会更新缓存实例的内部状态。
细节 3:“只有页面跳转才缓存” → 不完全对,「组件失活」都触发缓存(不止路由跳转)
路由跳转是最常见的 “组件失活” 场景,但不是唯一场景:
- 场景 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 失活 → 被缓存。
注意:使用v-show进行组件的隐藏时不会触发失活,keep-alive不会进行缓存
核心:只要 keep-alive 包裹的组件从 “渲染在页面上” 变为 “不渲染”,且符合 include/exclude 规则,就会被缓存。
细节 4:缓存的 VNode 包含什么?→ 不止节点 / 属性,还有组件的「完整实例状态」
VNode 是组件的 “虚拟描述”,缓存 VNode 本质是缓存组件实例:
- 包含 DOM 结构描述(比如
<div class="home">); - 包含组件的响应式数据(
data/computed/props); - 包含组件的 DOM 状态(输入框值、滚动条位置、复选框勾选状态);
- 包含组件的生命周期状态(不会再执行
created/mounted,而是执行activated)。
细节5: keep-alive 缓存的 “两个关键动作”
缓存不是 “一步到位”,而是分「持有实例」和「存入缓存」两步,对应不同阶段:
- 持有实例:组件首次加载(
mounted后),keep-alive会通过 VNode 关联组件实例,但不会立刻存入cache(此时组件激活,无需缓存兜底); - 存入缓存:组件首次失活(跳转 /
v-if=false),keep-alive才会把关联的实例正式存入cache,完成 “缓存” 的核心动作。
简单说:首次加载后,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 | 先失活,再销毁 |
是的你没看错,当keep-alive复用缓存的时候就只会触发actived这一层钩子,让人感觉起来展示的快,但是keep-alive跳过组件实例的创建、DOM 挂载、数据请求(若请求写在 mounted)等耗时操作,直接复用已有的组件实例和 DOM 结构,才是页面加载变快的核心原因。
最终本质
keep-alive 的核心是 “保活” 而非 “预缓存”:它不会主动缓存正在使用的激活组件,而是 “挽留” 即将被销毁的失活组件,将其存入缓存;当组件再次激活时,直接复用缓存实例,避免重复创建 / 销毁,既保留组件状态,又提升渲染性能。
简单记:keep-alive 不缓存 “活着” 的组件,只 “救活” 即将 “死掉” 的组件,复用 “救活” 的组件 —— 失活存缓存,激活用缓存,这是它的核心逻辑。