14.1.3 缓存管理
前两节的缓存「只进不出」,极端情况下会占满内存;本节给缓存加上容量上限与淘汰策略,并讨论把缓存管理权交给用户。
一、问题:缓存会无限增长
// 14.1.1 的实现:缓存不存在就写入
const cachedVNode = cache.get(rawVNode.type)
if (cachedVNode) {
// 激活
} else {
cache.set(rawVNode.type, rawVNode) // 只会新增,从不删除
}
被 KeepAlive 包裹的组件越多,cache 越大,极端情况下占用大量内存(且被缓存组件的 DOM 一直躺在隐藏容器里)。因此必须设置一个缓存阈值,超过阈值时对缓存进行修剪。
二、max:最大缓存数 + 「最近一次访问」淘汰
- 通过 KeepAlive 的
max属性设置最大缓存数; - 修剪策略:把当前正在渲染的组件视为「最新一次访问」,它在修剪过程中始终是安全的、不会被淘汰;需要淘汰时,挑最久没被访问的那个。
参考实现(在 14.1.1 的 render 函数里补充两处):
const KeepAlive = {
__isKeepAlive: true,
props: {
include: RegExp,
exclude: RegExp,
max: Number // 最大缓存数
},
setup(props, { slots }) {
// Map 的迭代顺序 = 插入顺序,可用来表达「访问先后」
const cache = new Map()
// ...省略 keepAliveCtx / storageContainer / _activate / _deActivate
return () => {
const rawVNode = slots.default()
if (typeof rawVNode.type !== 'object') return rawVNode
const cachedVNode = cache.get(rawVNode.type)
if (cachedVNode) {
rawVNode.component = cachedVNode.component
rawVNode.keptAlive = true
// 重新插入一次,把自己挪到 Map 末尾 → 它就是「最近被访问」的组件
cache.delete(rawVNode.type)
cache.set(rawVNode.type, rawVNode)
} else {
cache.set(rawVNode.type, rawVNode)
}
// 超出最大缓存数:淘汰最久没被访问的(Map 的第一项)
if (props.max && cache.size > props.max) {
const key = cache.keys().next().value
cache.delete(key)
// 注意:若该组件此刻正失活(DOM 在隐藏容器里),
// 还应真正卸载它以释放 DOM,仅删除缓存项并不释放内存
}
rawVNode.shouldKeepAlive = true
rawVNode.keepAliveInstance = instance
return rawVNode
}
}
}
要点:
- LRU 思路:Map 保持插入顺序,
cache.keys().next().value就是「最久未访问」的那一项; - 每次命中缓存时 delete + set,把自己移到队尾,等于刷新「访问时间」;
- 修剪必须发生在决定本次渲染之后,这样当前渲染的组件永远不会被自己淘汰。
三、把缓存策略交给用户(自定义缓存)
在官方 RFC 中已有相关提议:允许用户实现自定义的缓存策略,配套接口层面就是 KeepAlive 新增了 cache 接口,让用户传入自定义的缓存实例 —— 等价于把缓存的管理权限从 KeepAlive 组件转交给用户。
用户只需提供一个「类 Map」的缓存对象:
const _cache = new Map()
const cache = {
get(key) {
return _cache.get(key)
},
set(key, value) {
_cache.set(key, value)
},
delete(key) {
_cache.delete(key)
},
forEach(fn) {
_cache.forEach(fn)
}
}
<KeepAlive :max="2" :cache="cache">
<component :is="dynamicComp" />
</KeepAlive>
用户可以在 set / delete 里加入自己的淘汰规则(按权重、按时间、按内存占用等),KeepAlive 只负责调用这套接口。
四、小结
| 目标 | 做法 |
|---|---|
| 限制缓存规模 | 用 max 设置最大缓存数,超出即修剪 |
| 淘汰谁 | 淘汰最久未被访问的组件(LRU) |
| 保护当前组件 | 命中缓存时刷新其访问顺序,修剪在渲染之后进行 |
| 更灵活的规则 | 通过 cache 接口把缓存实例交给用户自定义 |
易错点:淘汰只是从
cache里删掉引用,不等于卸载组件;已被失活到隐藏容器的组件若不额外卸载,DOM 仍会驻留内存。
相关
- 14.1.1 组件的激活与失活:缓存的写入时机与
keptAlive/shouldKeepAlive标记 - 14.1.2 include 和 exclude:按组件名决定「该不该缓存」