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:缓存组件的生命周期钩子