Vue keep-alive 原理全解:LRU 缓存策略、源码级理解

用 keep-alive 的时候,你有没有想过:为什么设置了 max 之后,被淘汰的总是那个"最久没用"的组件?今天从 LRU 策略到源码实现,帮你彻底搞懂。


前言

keep-alive 这个东西,大部分 Vue 开发者都用过。包裹一下 <router-view>,页面切换时状态就保留了,用户体验确实好很多。

但说实话,我用了很久 keep-alive,一直有个疑问:设置 max 之后,Vue 是怎么决定淘汰哪个组件的?

后来翻了一下源码,发现里面用了一个经典的数据结构策略 ------ LRU(Least Recently Used,最近最少使用)。理解了这个,keep-alive 的所有行为就都说得通了。


一、LRU 策略:一句话讲清楚

当缓存满了,优先踢掉最久没被访问的那个。

举个例子,假设 max = 3

核心思想:最近用过的数据,将来大概率还会被用到。所以淘汰"最久没用"的,保留"最近用过"的。

这个策略在操作系统页面置换、Redis 缓存、浏览器缓存里都有应用,Vue 的 keep-alive 也是同样的思路。


二、keep-alive 使用指南

2.1 三个核心属性

属性 类型 作用
include 字符串/正则/数组 只有匹配的组件才缓存
exclude 字符串/正则/数组 匹配的组件不缓存
max 数字 最多缓存多少个组件实例

2.2 基本用法:缓存动态组件

html 复制代码
<keep-alive>
  <component :is="currentView"></component>
</keep-alive>

切换组件时,被切走的组件不会被销毁,状态完整保留。再切回来时,直接恢复之前的状态。

2.3 条件缓存:include 和 exclude

实际项目中,不是所有页面都需要缓存。比如搜索结果页、表单提交成功页,每次进去都应该是全新的状态。

html 复制代码
<!-- 只缓存列表页和详情页,不缓存搜索页 -->
<keep-alive include="UserList,UserDetail" exclude="SearchResult">
  <component :is="currentView"></component>
</keep-alive>

也可以用正则:

html 复制代码
<keep-alive :include="/^User/">
  <component :is="currentView"></component>
</keep-alive>

注意includeexclude 匹配的是组件的 name 选项 ,不是路由名称。所以组件一定要写 name

javascript 复制代码
export default {
  name: 'UserList',  // keep-alive 靠这个匹配
  // ...
}

这个坑我踩过,组件没写 name,include 怎么配都不生效,排查了半天才发现

2.4 启用 LRU:设置 max

html 复制代码
<!-- 最多缓存 10 个组件实例 -->
<keep-alive :max="10">
  <router-view />
</keep-alive>

设置了 max 之后,LRU 策略就生效了。当缓存数量超过 max,最久没访问的组件会被销毁。

建议 :一定要设置 max!不设的话,所有被 keep-alive 包裹过的组件都会一直缓存,内存只会增不会减,时间长了页面会越来越卡。

2.5 与 Vue Router 配合

这是最常见的使用场景。Vue 2 和 Vue 3 写法稍有不同:

html 复制代码
<!-- Vue 3 写法 -->
<router-view v-slot="{ Component }">
  <keep-alive :include="cachedViews" :max="15">
    <component :is="Component" />
  </keep-alive>
</router-view>
html 复制代码
<!-- Vue 2 写法 -->
<keep-alive :include="cachedViews" :max="15">
  <router-view />
</keep-alive>

Vue 3 必须通过 v-slot 拿到 Component,再传给 <component :is>,因为 Vue 3 的 <router-view> 不再直接渲染组件。

如果需要动态控制缓存列表,可以结合 Pinia:

javascript 复制代码
// store/cacheStore.js
export const useCacheStore = defineStore('cache', () => {
  const cachedViews = ref([])

  function addView(view) {
    if (!cachedViews.value.includes(view)) {
      cachedViews.value.push(view)
    }
  }

  function removeView(view) {
    const index = cachedViews.value.indexOf(view)
    if (index > -1) {
      cachedViews.value.splice(index, 1)
    }
  }

  return { cachedViews, addView, removeView }
})

三、源码实现:LRU 是怎么工作的?

3.1 缓存的到底是什么?

首先搞清楚一点:keep-alive 缓存的不是 DOM,而是 VNode(虚拟节点)和组件实例

VNode 持有组件实例的引用,所以缓存了 VNode 就等于缓存了整个组件的所有状态:

  • 响应式数据(data)
  • 计算属性(computed)
  • 侦听器(watch)
  • DOM 结构
  • 子组件树

这就是为什么 keep-alive 能做到"切回来还是原来的样子"。

3.2 Vue 2 vs Vue 3 的实现差异

特性 Vue 2 Vue 3
缓存对象 Object.create(null) 纯对象 new Map()
访问记录 数组 [] new Set()
代码组织 组件选项对象 setup() 函数
源码位置 core/components/keep-alive.js runtime-core/src/components/KeepAlive.ts

Vue 3 用 Map + Set 替代了 Object + Array,查找和删除操作从 O(n) 优化到了 O(1),性能更好。

3.3 核心数据结构

typescript 复制代码
// Vue 3 源码简化
const cache = new Map()  // key → VNode,缓存组件
const keys = new Set()   // 记录访问顺序,实现 LRU
  • cache:存组件的 VNode,key 通常是组件的 name 或组件标签
  • keys:记录访问顺序,Set 的迭代顺序就是插入顺序

3.4 渲染流程

keep-alive 的渲染逻辑大致是这样的:

3.5 关键代码解读

缓存命中时

typescript 复制代码
if (cache.has(key)) {
  const cached = cache.get(key)
  
  // 复用缓存的组件实例和 DOM
  vnode.component = cached.component
  vnode.el = cached.el
  
  // LRU 核心:先删再加,把 key 移到 Set 末尾
  keys.delete(key)
  keys.add(key)
}

keys.delete(key) + keys.add(key) 就是 LRU 的精髓。Set 是按插入顺序排列的,删了再加就相当于移到了末尾,变成了"最近使用"。

缓存未命中时

typescript 复制代码
// 存入缓存
cache.set(key, vnode)
keys.add(key)

// 检查是否需要淘汰
if (max && keys.size > parseInt(max)) {
  // 获取 Set 中第一个元素(最久没用的)
  const oldestKey = keys.values().next().value
  
  // 从缓存和 keys 中移除
  keys.delete(oldestKey)
  cache.delete(oldestKey)
  // 后续会执行组件的 unmount
}

keys.values().next().value 就是拿到 Set 中最早插入的那个 key,也就是最久没被访问的组件。

组件卸载的"劫持"

这是 keep-alive 最巧妙的地方。当组件被切走时,keep-alive 不会真正销毁它,而是"劫持"了卸载过程:

typescript 复制代码
// 源码概念逻辑
if (shapeFlag & ShapeFlags.COMPONENT_SHOULD_KEEP_ALIVE) {
  // 不执行 unmount,而是 deactivate
  deactivate(vnode)
  // 把 DOM 移到隐藏容器,而不是真正删除
}

deactivate 做的事情:

  1. 把组件的 DOM 从页面上移除
  2. 不销毁组件实例,保留所有状态
  3. 触发 deactivated 生命周期钩子

所以组件虽然看不见了,但其实还"活着"。


四、activated 和 deactivated

被 keep-alive 包裹的组件有两个独有的生命周期钩子:

activated ------ 组件被激活时调用

适合做这些事情:

  • 刷新数据(从服务器拉最新数据)
  • 开启定时器
  • 恢复滚动位置
javascript 复制代码
export default {
  name: 'UserList',
  activated() {
    // 每次切回来都刷新数据
    this.fetchUsers()
    // 恢复滚动位置
    this.$nextTick(() => {
      window.scrollTo(0, this.scrollPosition)
    })
  },
  deactivated() {
    // 记住滚动位置
    this.scrollPosition = window.scrollY
    // 清理定时器
    clearInterval(this.refreshTimer)
  }
}

deactivated ------ 组件被停用时调用

适合做这些事情:

  • 清理定时器
  • 取消未完成的网络请求
  • 保存当前状态

注意createdmounted 只在组件第一次创建时执行。之后每次激活/停用,只会触发 activated / deactivated


五、常见问题

Q1: keep-alive 包裹的组件,created 和 mounted 执行几次?

A :只有第一次进入时执行。之后再进入只触发 activated

Q2: 怎么让某个页面不缓存?

A:三种方式:

  1. exclude 排除组件名
  2. 在路由 meta 里标记,动态控制 include 列表
  3. 在 beforeRouteLeave 里手动清除缓存

Q3: 缓存的组件数据过期了怎么办?

A :在 activated 里重新请求数据。这是最推荐的做法。

Q4: keep-alive 能和 transition 一起用吗?

A:可以,但要注意顺序:

html 复制代码
<!-- Vue 3 -->
<router-view v-slot="{ Component }">
  <transition name="fade" mode="out-in">
    <keep-alive :include="cachedViews">
      <component :is="Component" />
    </keep-alive>
  </transition>
</router-view>

transition 要包在外面,keep-alive 包在里面。反过来写的话,离开动画可能不会触发。


六、总结

概念 说明
缓存内容 VNode + 组件实例(不是 DOM)
淘汰策略 LRU(最近最少使用)
核心数据结构 Vue 3: Map + Set
max 属性 一定要设,不然内存只增不减
组件 name include/exclude 靠 name 匹配,别忘了写
生命周期 activated(激活)/ deactivated(停用)

一句话总结:keep-alive 通过 LRU 策略管理缓存,命中时复用组件实例,未命中时新建并存入缓存,满了就踢掉最久没用的。

有问题评论区见,觉得有帮助点个赞

相关推荐
会联营的陆逊3 小时前
html2canvas 1.4.1 在 iOS Safari 中生成图片卡住的问题排查与修复
前端
ZC跨境爬虫3 小时前
跟着 MDN 学CSS day_13 :(深入理解CSS中的元素尺寸调整)
前端·javascript·css·ui·html·tensorflow
plainGeekDev4 小时前
Android内存面试题:OOM都解决不了,性能优化从何谈起?
android·面试·kotlin
threelab4 小时前
Three.js 加载 3D Tiles 瓦片数据 | 三维可视化 / AI 提示词
开发语言·前端·javascript·人工智能·3d·着色器
百度地图开放平台4 小时前
我用百度地图 Skills 体系重构了物流调度系统,节省了 90% 的人力
前端·github
JavaAgent架构师4 小时前
前端AI工程化(九):AI Agent平台前端架构设计
前端·人工智能
梦想CAD控件5 小时前
网页端对DWG图纸进行预览与批注(CAD轻量化)
java·前端·javascript
代码煮茶5 小时前
Vue3 埋点实战 | 从 0 搭建前端用户行为埋点系统
vue.js
蝎子莱莱爱打怪5 小时前
👍🏻👍🏻6年381颗芯片+韬定律,华为重新定义半导体,为什么还有人喷???
后端·面试·程序员