Vue3 的 KeepAlive 组件 LRU 缓存淘汰算法与组件实例复用机制原理
字数 1299 2025-12-05 11:46:02
Vue3 的 KeepAlive 组件 LRU 缓存淘汰算法与组件实例复用机制原理
题目描述
KeepAlive 组件是 Vue3 中用于缓存组件实例的内置组件,它可以避免组件频繁销毁和重建,提升页面切换性能。本题目将深入解析 KeepAlive 如何基于 LRU(最近最少使用)算法管理缓存,以及如何实现组件实例的激活与失活生命周期。
解题过程
1. 核心设计目标
- 缓存组件实例(vnode.component 属性),避免重复渲染
- 设定缓存容量限制,防止内存泄漏
- 使用 LRU 算法在容量超出时淘汰最久未使用的实例
- 提供 include/exclude/max 等属性控制缓存策略
- 触发组件的 activated/deactivated 生命周期钩子
2. KeepAlive 的缓存数据结构
// 源码中的关键数据结构
const cache: Map<string, VNode> = new Map() // 缓存映射表
const keys: Set<string> = new Set() // 缓存键集合,维护访问顺序
每个缓存条目包含:
- key:由组件名称和可选key属性生成
- vnode:缓存的虚拟节点,包含组件实例
- 额外信息:如父组件引用、锚点位置等
3. LRU 算法的具体实现
步骤1:访问顺序维护
// 当缓存被访问时(命中缓存)
function get(key: string): VNode | undefined {
const cached = cache.get(key)
if (cached) {
// 更新访问顺序:删除后重新添加,使其成为"最新使用"
keys.delete(key)
keys.add(key) // 添加到集合末尾表示最近使用
return cached
}
}
步骤2:缓存淘汰策略
function pruneCacheEntry(key: string) {
const cached = cache.get(key)
if (cached) {
// 1. 销毁组件实例
unmount(cached.component!.subTree)
// 2. 从缓存中删除
cache.delete(key)
keys.delete(key)
}
}
// 当超出最大缓存数时的淘汰逻辑
function pruneOldestEntry() {
// keys 集合的第一个元素就是最久未使用的
const keyToDelete = keys.values().next().value
if (keyToDelete) {
pruneCacheEntry(keyToDelete)
}
}
步骤3:缓存添加逻辑
function set(key: string, vnode: VNode) {
cache.set(key, vnode)
keys.add(key)
// 检查是否超出最大缓存数
if (max && keys.size > parseInt(max as string)) {
pruneOldestEntry() // 淘汰最久未使用的
}
}
4. 组件实例的激活与失活机制
步骤1:缓存组件实例
// 当组件需要被缓存时
const instance = vnode.component!
// 存储组件实例的当前状态
instance.ctx.deactivate = (vnode: VNode) => {
// 1. 移动到隐藏容器
move(vnode, storageContainer, null, MoveType.LEAVE, parentSuspense)
// 2. 触发 deactivated 钩子
queuePostFlushCb(() => {
instance.isDeactivated = true
invokeArrayFns(instance.deactivated)
})
}
// 将实例标记为已停用
instance.isDeactivated = true
步骤2:激活缓存实例
// 当需要激活缓存组件时
const instance = vnode.component!
// 1. 从隐藏容器移回 DOM
move(vnode, container, anchor, MoveType.ENTER, parentSuspense)
// 2. 触发 activated 钩子
queuePostFlushCb(() => {
instance.isDeactivated = false
invokeArrayFns(instance.activated)
})
步骤3:隐藏容器的实现
// 创建一个隐藏的容器来存放失活的组件
const storageContainer = document.createElement('div')
// 移动组件的 DOM 到隐藏容器
function move(
vnode: VNode,
container: HostElement,
anchor: HostNode | null,
type: MoveType,
parentSuspense: SuspenseBoundary | null
) {
// 实际操作 DOM 的移动
if (type === MoveType.LEAVE) {
remove(vnode.children as VNode[], parentComponent, parentSuspense)
} else {
insert(vnode.children as HostNode, container, anchor)
}
}
5. 完整的缓存命中流程
场景:切换显示相同组件
-
初次渲染:
- 创建组件实例并挂载
- 生成缓存key(组件名称 + key属性)
- 存入缓存 Map
- 添加到 keys 集合末尾
-
切换到其他组件:
- 当前组件触发 deactivated 钩子
- 组件子树 DOM 移动到隐藏容器
- 组件实例保持完整状态
-
再次切回组件:
- 检查缓存是否存在对应key
- 命中缓存,从 keys 中删除并重新添加到末尾(LRU更新)
- 从隐藏容器移回 DOM
- 触发 activated 钩子
- 直接复用组件实例,避免重新创建
-
超出缓存容量:
- 从 keys 集合获取第一个(最久未使用)的key
- 销毁对应组件实例
- 从缓存中移除
- 添加新缓存项
6. 与 Vue 渲染器的集成
步骤1:在 patch 过程中处理
// 渲染器处理 KeepAlive 组件
const { shapeFlag, type } = n2
if (shapeFlag & ShapeFlags.COMPONENT) {
if (type === KeepAlive) {
// 特殊处理 KeepAlive
processKeepAlive(n1, n2, container, anchor, parentComponent)
}
}
步骤2:挂载和更新逻辑
function processKeepAlive(n1, n2, container, anchor, parentComponent) {
// 获取 KeepAlive 的默认插槽内容
const children = n2.children.default!()
const rawVNode = children[0]
// 生成缓存key
const key = rawVNode.key == null
? rawVNode.type
: rawVNode.key
// 检查缓存
const cachedVNode = cache.get(key)
if (cachedVNode) {
// 命中缓存:复用实例
rawVNode.component = cachedVNode.component
// 更新 LRU 顺序
keys.delete(key)
keys.add(key)
// 激活组件
rawVNode.shapeFlag |= ShapeFlags.COMPONENT_KEPT_ALIVE
} else {
// 未命中:缓存新实例
keys.add(key)
if (max && keys.size > max) {
pruneOldestEntry()
}
cache.set(key, rawVNode)
}
// 标记为 KeepAlive 组件
rawVNode.shapeFlag |= ShapeFlags.COMPONENT_SHOULD_KEEP_ALIVE
}
7. 性能优化细节
-
DOM 操作优化:
- 使用 document.createElement 创建隐藏容器
- 通过 insert/remove 操作移动 DOM 而非销毁重建
- 保持组件内部状态不丢失
-
内存管理:
- 默认无限制缓存,但提供 max 属性限制
- 使用 LRU 自动淘汰旧缓存
- 组件卸载时清理所有缓存
-
生命周期协调:
- deactivated 在组件移动到隐藏容器后异步触发
- activated 在组件移回 DOM 后异步触发
- 避免生命周期与 DOM 操作冲突
8. 特殊场景处理
-
动态组件:
<KeepAlive> <component :is="currentComponent" /> </KeepAlive>每个组件类型根据组件名称缓存
-
嵌套 KeepAlive:
Vue3 支持嵌套,每个 KeepAlive 独立管理自己的缓存 -
与 Transition 组件结合:
激活/失活动画正常执行,KeepAlive 不干扰过渡效果
这个机制确保了在路由切换或标签页切换等场景中,组件状态得以保留,同时通过 LRU 算法防止内存无限增长,实现了性能与内存占用的最佳平衡。