深入理解 Vue keep-alive:缓存本质、触发条件与生命周期对比

一、先明确核心结论

keep-alive 是 Vue 内置的抽象组件(不渲染真实 DOM ),它的核心作用是缓存被包裹的组件实例,缓存的关键数据结构如下:

  • 缓存容器:keep-alive 实例上的 this.cache(一个对象,key 是组件的「缓存标识」,value 是组件实例);
  • 辅助记录:this.keys(一个数组,存储缓存组件的 key,用于实现 max 缓存数量限制);
  • 挂载关系:被缓存的组件实例 → 作为 this.cache 对象的属性值 → 挂在 keep-alive 组件实例上,而非被缓存组件自己的实例上。

二、keep-alive 挂载缓存的完整过程(分步骤拆解)

以 Vue 2 为例(Vue 3 逻辑一致,仅源码实现细节略有差异),核心流程如下:

步骤 1:keep-alive 初始化,创建缓存容器

keep-alive 组件初始化时,会在自身实例上创建两个核心属性,用于存储缓存:

javascript 复制代码
// keep-alive 组件的初始化逻辑(简化版)
export default {
  name: 'keep-alive',
  abstract: true, // 抽象组件,不参与DOM渲染
  props: {
    include: [String, RegExp, Array], // 需缓存的组件
    exclude: [String, RegExp, Array], // 排除缓存的组件
    max: [String, Number] // 最大缓存数量
  },
  created() {
    this.cache = Object.create(null); // 缓存容器:{ key: 组件实例 }
    this.keys = []; // 缓存key列表:[key1, key2...]
  },
  // ...其他生命周期
}
  • this.cache :空对象,后续用来存「缓存标识 → 组件实例」的映射;
  • this.keys :空数组,记录缓存 key 的顺序,用于 LRU 淘汰(超出 max 时删除最久未使用的缓存)。

步骤 2:组件首次渲染,判断是否缓存

keep-alive 包裹的组件首次渲染时,keep-aliverender 函数会执行核心逻辑:

  1. 获取被包裹组件的**「缓存标识」**(key):
    • 默认 key:组件名 + 组件实例的uid(避免同组件不同实例冲突);
    • 自定义 key:可通过 key 属性指定(如 <keep-alive><component :is="comp" :key="compKey" /></keep-alive>)。
  1. 判断是否符合缓存规则(include / exclude):
    • 若符合:将组件实例存入 this.cache,并把 key 加入 this.keys
    • 若不符合:不缓存,直接渲染组件(和普通组件一样)。

举个例子:

xml 复制代码
<keep-alive>
      <router-view v-if="$route.meta.keepAlive" />
</keep-alive>

步骤 3:缓存组件实例,挂载到 keep-alive

核心逻辑简化如下:

kotlin 复制代码
// keep-alive 的 render 函数核心逻辑(简化版)
render() {
  const slot = this.$slots.default;
  const vnode = getFirstComponentChild(slot); // 获取被包裹的第一个组件vnode
  const componentOptions = vnode && vnode.componentOptions;
  
  if (componentOptions) {
    // 1. 生成缓存key(核心:唯一标识组件实例)
    const key = this.getCacheKey(vnode);
    const { cache, keys } = this;

    // 2. 判断是否需要缓存(符合include,不符合exclude)
    if (this.shouldCache(componentOptions)) {
      // 3. 若缓存中已有该组件实例,直接复用
      if (cache[key]) {
        vnode.componentInstance = cache[key].componentInstance;
        // 更新key顺序(LRU:把当前key移到最后,标记为最近使用)
        remove(keys, key);
        keys.push(key);
      } else {
        // 4. 首次渲染:将组件vnode(包含实例)存入缓存
        cache[key] = vnode;
        keys.push(key);
        // 5. 超出max时,删除最久未使用的缓存
        if (this.max && keys.length > parseInt(this.max)) {
          pruneCacheEntry(cache, keys[0], keys, this._vnode);
        }
      }
      // 标记组件为"被缓存",避免重复初始化
      vnode.data.keepAlive = true;
    }
  }
  return vnode;
}

关键挂载动作cache[key] = vnode → 组件的 vnode(包含 componentInstance 即组件实例)被作为 cache 对象的属性值,挂载到 keep-alive 实例的 this.cache 上。

步骤 4:组件再次渲染,复用缓存实例

当被缓存的组件需要再次渲染时(比如路由切换后返回):

  1. keep-alive this.cache 中根据 key 取出对应的组件实例;
  2. 将缓存的实例赋值给新的 vnode 的 componentInstance
  3. 直接复用该实例渲染,不再执行组件的 created / mounted 等生命周期(而是触发 activated 钩子)。

三、关键细节:为什么缓存不挂在被缓存组件自己的实例上?

  1. 逻辑合理性keep-alive 是「缓存管理者」,应该由管理者统一存储和管理所有被缓存的组件,而非让组件自己存储;
  2. 避免内存泄漏 :若缓存挂在被缓存组件实例上,组件实例本身无法被销毁(因为缓存引用了它),而 keep-alive 统一管理可通过 maxexclude 主动清理缓存;
  3. 多实例隔离 :多个 keep-alive 组件的缓存是隔离的(比如页面 A 和页面 B 各有一个 keep-alive),每个 keep-alive 实例有自己的 cache,不会互相干扰。

总结:

一、先肯定你的正确认知

  1. keep-alive 是 Vue 官方内置的抽象组件(不渲染真实 DOM),被包裹的组件实例 / 状态会挂载在keep-alive 实例的 cache 对象上(而非组件自身);
  2. ✅ 缓存的核心内容是组件的 VNode (包含组件实例、DOM 节点描述、数据状态如 data/props/ 输入框值等);
  3. ✅ 组件的缓存触发和「组件失活」强相关(比如路由跳转导致组件被隐藏)。

二、需要修正 / 补充的关键细节

细节 1:"组件经过 keep-alive 时被缓存" → 不是 "经过",而是 "组件被 keep-alive 包裹且失活时才缓存"

keep-alive 不会 "主动拦截" 组件,而是当被包裹的组件从「激活状态」变为「失活状态」时,才会将其 VNode 存入缓存(而非加载时就缓存)。

  • 激活状态:组件在页面中可见(比如当前路由匹配的 Home 组件);
  • 失活状态:组件被隐藏(比如跳转到 List 路由,Home 组件被 router-view 卸载)。

简单说:keep-alive 是 "挽留" 即将被销毁的组件 ------ 默认情况下,组件失活会被销毁,而 keep-alive会把它存入缓存,避免销毁。

细节 2:"只有页面跳转才缓存" → 不完全对,「组件失活」都触发缓存(不止路由跳转)

路由跳转是最常见的 "组件失活" 场景,但不是唯一场景:

  • 场景 1(路由跳转):/home/list,Home 组件失活 → 被缓存;
  • 场景 2(条件渲染):<keep-alive><component :is="compName" /></keep-alive>,当compNameHome 改为 List 时,Home 失活 → 被缓存;
  • 场景 3(v-if 隐藏):<keep-alive><div v-if="show">Home组件</div></keep-alive>,当showtrue 改为 false 时,Home 失活 → 被缓存。

核心:只要 keep-alive 包裹的组件从 "渲染在页面上" 变为 "不渲染",且符合 include/exclude 规则,就会被缓存。

细节 3:"只加载页面不跳转,不会缓存" → 准确说:"组件未失活,缓存容器中已有该组件的 VNode,但未触发「缓存复用」"

即使不跳转,只要组件被 keep-alive 包裹并完成首次渲染:

  1. keep-alive cache 已经存入了该组件的 VNode (可以通过前面的代码查到);
  2. 只是因为组件未失活,所以不会触发 activated 钩子,也不会体现出 "缓存效果"(比如输入框输入内容,不跳转的话,内容本来就在,看不出缓存);
  3. 只有当组件失活后再次激活(比如跳转回来),才会从缓存中复用 VNode,此时能看到 "状态保留"(比如输入框内容还在)------ 这才是缓存的 "可见效果"。

举个直观例子:

  • 步骤 1:访问 /home,Home 组件渲染(激活),keep-alive.cache 中已有 Home 的 VNode(但未体现缓存);
  • 步骤 2:在 Home 输入框输入 "123",跳转到 /list(Home 失活),keep-alive 保留 Home 的 VNode(包含输入框的 "123");
  • 步骤 3:跳回 /home(Home 激活),keep-alive 复用缓存的 VNode,输入框仍显示 "123"------ 这就是缓存的效果。

如果只停留在步骤 1(不跳转),虽然缓存容器中有 Home 的 VNode,但因为没有 "失活→激活" 的过程,所以看不到缓存的效果,并非 "没有缓存"。

细节 4:缓存的 VNode 包含什么?→ 不止节点 / 属性,还有组件的「完整实例状态」

VNode 是组件的 "虚拟描述",缓存 VNode 本质是缓存组件实例:

  • 包含 DOM 结构描述 (比如 <div class="home">);
  • 包含组件的响应式数据data/computed/props);
  • 包含组件的 DOM 状态(输入框值、滚动条位置、复选框勾选状态);
  • 包含组件的生命周期状态不会再执行 created / mounted ,而是执行 activated)。

三、总结:精准理解 keep-alive 的缓存逻辑

  1. 挂载关系keep-alive 是 "缓存管理者",被包裹组件的 VNode(含实例 / 状态)挂载在 keep-alive实例的 cache 对象上;
  2. 缓存触发 :组件被 keep-alive 包裹 + 组件从「激活→失活」(路由跳转 / 条件隐藏等)→ 存入缓存;
  3. 缓存复用 :组件从「失活→激活」→ 从 cache 中取出 VNode 复用(不重新创建实例,保留状态);
  4. 可见效果:只有 "失活→激活" 的过程,才能体现缓存(状态保留),仅加载组件不跳转,缓存存在但无 "可见效果"。

简单记: keep-alive 的核心是 "保活"------ 不让失活的组件销毁,而是存入缓存,下次激活时直接复用,避免重复创建 / 销毁,同时保留组件状态。


keep-alive组件加载生命周期对比

阶段 Vue 2 生命周期 Vue 3 组合式 API 核心特点
首次加载 beforeCreate → created → beforeMount → mounted → activated setup → onBeforeMount → onMounted → onActivated 完整生命周期,最后触发激活钩子
失活缓存 deactivated onDeactivated 仅触发失活钩子,不销毁组件
二次加载 activated onActivated 仅触发激活钩子,跳过创建 / 挂载
缓存销毁 deactivated → beforeDestroy → destroyed onDeactivated → onBeforeUnmount → onUnmounted 先失活,再销毁
相关推荐
|晴 天|1 小时前
WebAssembly:为前端插上性能的翅膀
前端·wasm
孟祥_成都1 小时前
你可能不知道 react 组件中受控和非受控的秘密!
前端
火车叼位1 小时前
ast-grep:结构化搜索与重构利器
前端
over6971 小时前
深入理解 JavaScript 原型链与继承机制:从 instanceof 到多种继承模式
前端·javascript·面试
烂不烂问厨房1 小时前
前端实现docx与pdf预览
前端·javascript·pdf
GDAL1 小时前
Vue3 Computed 深入讲解(聚焦 Vue3 特性)
前端·javascript·vue.js
Moment1 小时前
半年时间使用 Tiptap 开发一个和飞书差不多效果的协同文档 😍😍😍
前端·javascript·后端
前端加油站1 小时前
记一个前端导出excel受限问题
前端·javascript
da_vinci_x1 小时前
PS 生成式扩展:从 iPad 到带鱼屏,游戏立绘“全终端”适配流
前端·人工智能·游戏·ui·aigc·技术美术·游戏美术