keep-alive 是 Vue 2 运行时里最常被提及却最少被深究的内置组件。它看上去只是"把页面缓存起来",背后却涉及实例生命周期劫持、LRU 缓存策略、VNode 复用以及内存管理。
一、设计动机
单页应用里常见的"标签页""面包屑""分步表单"等交互模式,都要求用户在多个路由或状态之间来回切换。默认情况下,每一次切换都会触发旧组件的 $destroy
,再执行新组件的完整挂载链路:
beforeDestroy → destroy → 回收 DOM → 创建 DOM → beforeCreate → created → mounted
如果页面重、接口多,这种"拆房再建房"的代价极高,而且无法保留滚动位置、表单草稿等瞬态状态。
keep-alive 通过缓存组件实例(而非仅缓存 DOM)解决了两个问题:
- 时间成本:跳过创建与销毁,直接复用已有实例;
- 状态保留:实例存活,内部 state、DOM 引用、定时器全部保持原状。
二、组件级缓存的三要素
keep-alive 在 created
钩子里初始化两个核心字段:
js
this.cache = Object.create(null) // 组件缓存池
this.keys = [] // 缓存键的有序索引
缓存池是一个键到 VNode 的映射,键的生成规则如下:
- 若组件在路由或
key
prop 中显式声明,则直接使用; - 否则使用
cid + "::" + tag
自动生成,确保全局唯一。
keys
数组用来维护 LRU 顺序:最近一次被命中的键总是被移动到数组末尾,最久未使用的键位于头部,当缓存数量超过 max
时直接 shift
掉。
三、渲染函数:命中、失活、淘汰的决策点
keep-alive 没有模板,它的逻辑全部写在 render
:
js
render () {
const vnode = getFirstComponentChild(this.$slots.default)
const key = vnode.key == null
? generateKey(vnode)
: vnode.key
const { cache, keys } = this
if (cache[key]) { // ------ 命中缓存
vnode.componentInstance = cache[key].componentInstance
remove(keys, key)
keys.push(key) // 维护 LRU
} else { // ------ 首次出现
cache[key] = vnode
keys.push(key)
if (this.max && keys.length > this.max) {
pruneCacheEntry(cache, keys[0]) // 淘汰最久未使用
}
}
vnode.data.keepAlive = true // 打上标记,阻止后续销毁
return vnode
}
关键点拆解:
- 复用实例:直接复用
componentInstance
,包括内部状态、DOM 引用、事件监听。 - 阻止销毁:子组件在
destroy
阶段会检查vnode.data.keepAlive
,为真则跳过$destroy
,仅执行deactivated
。 - LRU 淘汰:
pruneCacheEntry
会手动调用$destroy
并移除 DOM,确保内存不会无限膨胀。
四、生命周期劫持:activated 与 deactivated
被 keep-alive 包裹的组件新增两条专用钩子:
- activated:组件从缓存池取出并插入 DOM 后触发;首次挂载在
mounted
之后立即执行一次。 - deactivated:组件从 DOM 移除但实例存活时触发,此时 DOM 已卸载,定时器、事件监听仍可运行。
常见用法:
js
activated () {
this.$refs.scroll && this.$refs.scroll.restore()
},
deactivated () {
this.timer && clearInterval(this.timer)
}
五、缓存命中时的完整调用链
用户从路由 A 切换到路由 B,再回退到 A:
- 路由 A 的组件失活 →
deactivated
- 路由 B 挂载 → 正常生命周期
- 回退到 A → keep-alive 发现 key 命中
- 取出旧实例 →
activated
→ DOM 重新插入 → 页面瞬间恢复
整个过程无 beforeCreate / created / mounted
,也无 DOM 重建,仅有 CSS 动画或滚动恢复逻辑。
六、内存与边界注意事项
- max 必须设置:未设置时缓存无限增长,切页面多会撑爆内存。
- 避免缓存庞大状态:缓存的是实例 + DOM 树,包含所有闭包变量;大数据列表或第三方图表应手动
deactivated
中销毁。 - keep-alive 不能缓存异步组件本身,只能缓存异步组件解析后的真实组件实例;若需缓存加载态,把
<Suspense>
与 keep-alive 组合使用。 - include / exclude 支持正则与函数,可基于路由 meta 动态调整缓存策略,实现"登录页不缓存,业务页缓存"。
七、代码实例
vue
<template>
<div>
<button @click="id = id === 'a' ? 'b' : 'a'">toggle</button>
<keep-alive :max="10" include="A,B">
<comp-a v-if="id === 'a'" />
<comp-b v-else />
</keep-alive>
</div>
</template>
切换路由或条件渲染时,<comp-a>
和 <comp-b>
的实例被缓存;max
限制为 10,超出后最早访问的组件会被销毁。