Keep-Alive 实现原理
Vue Keep-Alive 组件的 LRU 缓存机制
问题
Vue 的 <keep-alive> 组件是如何缓存组件实例的?它使用的 LRU 缓存算法是什么原理?
解答
Keep-Alive 基本用法
<template>
<!-- 缓存动态组件 -->
<keep-alive :max="10" :include="['ComponentA', 'ComponentB']">
<component :is="currentComponent" />
</keep-alive>
</template>
LRU 算法原理
LRU(Least Recently Used)是一种缓存淘汰策略:当缓存满时,优先淘汰最久未使用的数据。
访问顺序: A -> B -> C -> A -> D (max=3)
初始: [A]
访问B: [A, B]
访问C: [A, B, C]
访问A: [B, C, A] // A 被访问,移到最后
访问D: [C, A, D] // 缓存满,淘汰最久未用的 B
手写 LRU 缓存
class LRUCache {
constructor(capacity) {
this.capacity = capacity
// Map 保持插入顺序,最早插入的在前面
this.cache = new Map()
}
get(key) {
if (!this.cache.has(key)) {
return -1
}
// 访问后移到最后(最近使用)
const value = this.cache.get(key)
this.cache.delete(key)
this.cache.set(key, value)
return value
}
put(key, value) {
// 如果已存在,先删除
if (this.cache.has(key)) {
this.cache.delete(key)
}
// 添加到最后
this.cache.set(key, value)
// 超出容量,删除最早的(第一个)
if (this.cache.size > this.capacity) {
const firstKey = this.cache.keys().next().value
this.cache.delete(firstKey)
}
}
}
// 测试
const cache = new LRUCache(2)
cache.put(1, 'a')
cache.put(2, 'b')
console.log(cache.get(1)) // 'a',1 变成最近使用
cache.put(3, 'c') // 淘汰 2
console.log(cache.get(2)) // -1,2 已被淘汰
Keep-Alive 源码简化实现
// Vue 3 Keep-Alive 简化版
const KeepAlive = {
name: 'KeepAlive',
props: {
include: [String, RegExp, Array],
exclude: [String, RegExp, Array],
max: [String, Number]
},
setup(props, { slots }) {
// 缓存组件 VNode
const cache = new Map()
// 记录缓存的 key 集合,用于 LRU
const keys = new Set()
// 修剪缓存
function pruneCacheEntry(key) {
const cached = cache.get(key)
// 卸载组件实例
if (cached) {
unmount(cached)
}
cache.delete(key)
keys.delete(key)
}
return () => {
const vnode = slots.default?.()[0]
if (!vnode) return null
const comp = vnode.type
const key = vnode.key ?? comp
// 检查 include/exclude
const name = comp.name
if (
(props.include && !matches(props.include, name)) ||
(props.exclude && matches(props.exclude, name))
) {
return vnode
}
const cachedVNode = cache.get(key)
if (cachedVNode) {
// 命中缓存,复用组件实例
vnode.component = cachedVNode.component
// LRU: 移到最后
keys.delete(key)
keys.add(key)
} else {
// 未命中,添加到缓存
cache.set(key, vnode)
keys.add(key)
// 超出 max,淘汰最久未使用的
if (props.max && keys.size > parseInt(props.max)) {
const firstKey = keys.values().next().value
pruneCacheEntry(firstKey)
}
}
// 标记为 keep-alive 组件
vnode.shapeFlag |= ShapeFlags.COMPONENT_KEPT_ALIVE
return vnode
}
}
}
// 匹配函数
function matches(pattern, name) {
if (Array.isArray(pattern)) {
return pattern.includes(name)
} else if (typeof pattern === 'string') {
return pattern.split(',').includes(name)
} else if (pattern instanceof RegExp) {
return pattern.test(name)
}
return false
}
双向链表 + 哈希表实现(O(1) 复杂度)
// 链表节点
class ListNode {
constructor(key, value) {
this.key = key
this.value = value
this.prev = null
this.next = null
}
}
class LRUCache {
constructor(capacity) {
this.capacity = capacity
this.map = new Map() // key -> node,O(1) 查找
// 虚拟头尾节点,简化边界处理
this.head = new ListNode(0, 0)
this.tail = new ListNode(0, 0)
this.head.next = this.tail
this.tail.prev = this.head
}
// 将节点移到链表头部(最近使用)
moveToHead(node) {
this.removeNode(node)
this.addToHead(node)
}
// 删除节点
removeNode(node) {
node.prev.next = node.next
node.next.prev = node.prev
}
// 添加到头部
addToHead(node) {
node.next = this.head.next
node.prev = this.head
this.head.next.prev = node
this.head.next = node
}
// 删除尾部节点(最久未使用)
removeTail() {
const node = this.tail.prev
this.removeNode(node)
return node
}
get(key) {
if (!this.map.has(key)) return -1
const node = this.map.get(key)
this.moveToHead(node)
return node.value
}
put(key, value) {
if (this.map.has(key)) {
const node = this.map.get(key)
node.value = value
this.moveToHead(node)
} else {
const node = new ListNode(key, value)
this.map.set(key, node)
this.addToHead(node)
if (this.map.size > this.capacity) {
const removed = this.removeTail()
this.map.delete(removed.key)
}
}
}
}
关键点
- LRU 策略:淘汰最久未使用的缓存,保留最近访问的
- Map 有序性:ES6 Map 按插入顺序迭代,可直接实现 LRU
- 双向链表 + 哈希表:get/put 都是 O(1) 时间复杂度
- include/exclude:通过组件名过滤是否缓存
- max 属性:限制缓存数量,超出时触发 LRU 淘汰
- activated/deactivated:缓存组件的生命周期钩子
目录