一次 “简单需求“ 引发的 Vue 缓存血案:从 keep-alive 踩坑到 Vue3 优化全记录

文章目录

    • 前言:一次“简单需求”引发的技术深挖
    • 一、问题爆发:缓存有也不是事,没也不是事
    • 二、原理拆解:keep-alive 到底是怎么“记住你”的?
      • 1. 它缓存的不是页面,是“组件实例”
      • 2. 那它怎么判断“谁”该被缓存?
      • 3. 那你说路由的 `meta.keepAlive` 起什么作用?
      • 4. 动态 include:缓存的核心控制力
      • 5. 如果你没写 include,会发生什么?
      • 小结:缓存是否生效,取决于这三要素
      • 推荐 key 的正确写法
    • 三、进阶实战:用 Vuex + keep-alive 构建你的缓存调度中心
      • 背景设定
      • 目录结构(Vue2 + Vuex 项目)
      • Step 1:每个组件要定义 `name`
      • Step 2:Vuex 管理缓存列表
      • Step 3:根据路由 tab 增删缓存(假设你有一个 `tagsView` 标签页栏)
      • Step 4:AppMain.vue 中的缓存核心逻辑
      • Step 5:刷新页面的正确姿势(强制卸载 + 重建)
      • 总结:Vue2 缓存调度核心五件套
    • 四、Vue3 的缓存机制进化:更现代、更透明、更易控
      • 1. 概览:keep-alive 在 Vue3 中的变化
      • 2. 生命周期:从 Options API 到 Composition API
      • 3. 缓存控制的“现代方式”:响应式 include 列表
      • 4. 更精准的组件识别:setup 组件也支持缓存
      • 5. Vue3 + Suspense + Teleport,缓存扩展玩法更多
      • 小结:Vue2 vs Vue3 缓存机制对比图谱
    • 总结

前言:一次“简单需求”引发的技术深挖

事情是这样的——因为团队人手紧张,我临时接手了一个三年前的老项目,技术栈是 Vue2。需求提了个看似简单的需求:“我们这个部分页面能不能加个缓存?别每次切换回来都重新加载了。”

我心想:“这年头了,Vue 的缓存不是一句 keep-alive 就搞定?”

于是信手一加,页面状态完美保留,效果看起来相当不错。我刚准备喝口茶,结果测试同学一句话把我拉了回来:

“咦?你这个 tab 不是关掉了吗?怎么再点回来状态还在?这缓存清不了啊!”

我:“啊这……”

一开始我以为是 key 写错了,后来排查路由、组件、Vuex,越查越不对劲。原来,Vue2 的缓存机制可不是“加了就有”,关键在于:你还得“能控”。

就这样,一个简单的 keep-alive,让我卷进了缓存的世界,开始了一场追踪组件生命周期、解构 include 名称匹配、调和业务 tab 与缓存机制之间矛盾的技术旅程。

所以这篇文章,不仅是为了实现“缓存生效”,更重要的是:

如何优雅控制缓存何时启用、何时销毁?Vue2 的缓存机制有哪些坑?Vue3 又做了哪些改进?在 tab 标签页场景下,缓存策略到底该怎么设计?

别急,马上我们就来一探究竟 ——走进 Vue 的缓存世界,重新认识你以为“已经懂了”的 keep-alive


一、问题爆发:缓存有也不是事,没也不是事

当你接到“做个页面缓存”的需求,第一个想到的当然就是给 router-view 包个


  

我们锁定它的位置:在 layout/components/AppMain.vue。没错,它就藏在这里。

这个写法确实能让页面缓存起来,状态也保得住,看上去一切正常,但——你关掉 tab,再点回来,它还在,页面状态一点没变,缓存“死死不放手”。

聪明的小伙伴马上会发现问题:它的缓存机制只依赖于路由的 meta.keepAlive,只要路由说“我需要缓存”,那这个组件就一直活在内存里,除非你手动刷新整个页面或关掉浏览器。

于是我开始溯源,试图找到这个实现的“前身”。看着目录结构、组件命名方式,猛地一拍大腿:

“这不是 Vue-Element-Admin 的路子吗?”

果然,扒一扒它的源代码,发现了真正的“缓存控制中枢”:

一次 “简单需求“ 引发的 Vue 缓存血案:从 keep-alive 踩坑到 Vue3 优化全记录_第1张图片
在它的 AppMain.vue 中,include 并不是直接根据路由,而是通过 Vuex 中的 cachedViews 动态控制:

  
    
  

你看人家就做得很细:

  • 当前标签页打开了,加入缓存;
  • 标签页关闭了,从缓存里移除;
  • 缓存的核心逻辑掌握在自己手里,而不是交给死板的路由 meta。

那么问题来了:

为什么你接手的项目,从“原版 Vue-Element-Admin”改成了只用 meta.keepAlive

是出于简化结构?还是历史遗留?没人知道。只知道这个“轻松写法”现在让你头大。

在继续之前,我们要从“缓存机制的本质”入手,重新梳理:

  • keep-alive 到底缓存谁、怎么缓存?
  • 如何让它听话地“清”和“留”?
  • 多标签页(Tabs)场景下,缓存如何优雅运作?

接下来,我们将进入Vue2 的 是怎么工作的,从底层机制开始抽丝剥茧,构建我们自己的“缓存掌控术”。

二、原理拆解:keep-alive 到底是怎么“记住你”的?

你以为 就是个“组件容器”?
不,它是个内存缓存管理器。你一不小心,它就把你的页面塞进了“长期保存区”,永不销毁——这不是玄学,是机制。


1. 它缓存的不是页面,是“组件实例”

Vue 的 是个抽象组件,它不会渲染 DOM,它的唯一任务是:

把被包裹的组件实例缓存到内存中,并在需要时恢复它。

一旦组件被缓存,它的生命周期就不再完整执行,而是触发特定的“复活钩子”:

生命周期钩子 说明
activated 组件从缓存中被激活(再次显示)
deactivated 组件被缓存但不是销毁(暂时隐藏)

2. 那它怎么判断“谁”该被缓存?

这一步是重头戏,Vue2 是通过组件的 name 进行匹配的:


  

组件自身必须定义 name,否则无法被识别:

export default {
  name: 'UserList'
}

⛔️ 坑点:如果 name 缺失 or 写错,缓存直接失效,还不报错!


3. 那你说路由的 meta.keepAlive 起什么作用?

它其实是我们开发者约定的“提示标签”:

{
  path: '/user/list',
  component: UserList,
  meta: { keepAlive: true }
}

Vue 本身不认识这个字段,是你在 AppMain.vue 或其他地方通过它做判断:


  

说白了:路由 meta 就是决定“要不要包 ”,但真正决定“能不能缓存”的,是 name 和 include。


4. 动态 include:缓存的核心控制力

真正的掌控来自这里:


  

cachedViews 可能来自 Vuex:

computed: {
  cachedViews() {
    return this.$store.state.tagsView.cachedViews
  }
}

每打开一个标签页,就向其中 push 对应组件的 name;每关闭标签页,就 remove 对应 name,这样缓存组件自动增删。


5. 如果你没写 include,会发生什么?

Vue 会默认缓存所有曾经加载过的组件,直到页面刷新、或浏览器进程结束。这就是你遇到“缓存死活清不掉”的真正原因!


小结:缓存是否生效,取决于这三要素

条件 是否必须 作用
组件有 name ✅ 是 提供唯一标识
使用 ✅ 是 启用缓存机制
include 正确匹配 推荐 控制缓存清单
meta.keepAlive ❌ 非必须 仅用于条件判断

推荐 key 的正确写法

避免缓存错乱或异常,可以使用更细粒度的 key


  • $route.path 只看路径,不包含参数;
  • $route.fullPath 更细致,含 query 等,可区分多个同路径不同参数的页面。

下一节,我们将动手实现:用 Vuex + keep-alive 构建你的缓存调度中心

我们会:

  • 实现一个标签页系统(tabs);
  • 配套 Vuex 存储 cachedViews
  • 页面打开、关闭、切换时动态增删缓存;
  • 支持手动清除缓存、强刷页面等高级操作。

将是整个 keep-alive 篇的核心,我们不再纸上谈兵,而是撸起袖子实现一个动态可控的缓存机制,告别“缓存失控、页面重载、状态丢失”三连崩溃。


三、进阶实战:用 Vuex + keep-alive 构建你的缓存调度中心

背景设定

我们的目标是实现如下功能:

每打开一个路由 tab,就缓存对应组件
每关闭一个 tab,就移除缓存
支持多标签页切换不刷新
支持手动刷新页面(重载组件)
所有缓存逻辑集中管理,易扩展、可追踪

核心关键词:Vuex、cachedViews、组件 name、keep-alive include


目录结构(Vue2 + Vuex 项目)

src/
├── layout/
│   └── components/
│       └── AppMain.vue      # 路由出口 + 缓存处理
├── store/
│   └── modules/
│       └── tagsView.js      # tab 管理 + 缓存列表
├── router/
│   └── index.js             # 设置 meta.keepAlive
├── views/
│   └── YourPage.vue         # 必须定义 name!

Step 1:每个组件要定义 name

// views/UserList.vue
export default {
  name: 'UserList',
  ...
}

否则 keep-alive 根本识别不了该组件!


Step 2:Vuex 管理缓存列表

// store/modules/tagsView.js
const state = {
  cachedViews: []
}

const mutations = {
  ADD_CACHED_VIEW(state, view) {
    if (state.cachedViews.includes(view.name)) return
    state.cachedViews.push(view.name)
  },
  DEL_CACHED_VIEW(state, view) {
    const index = state.cachedViews.indexOf(view.name)
    if (index > -1) state.cachedViews.splice(index, 1)
  },
  CLEAR_CACHED_VIEWS(state) {
    state.cachedViews = []
  }
}

export default {
  namespaced: true,
  state,
  mutations
}

Step 3:根据路由 tab 增删缓存(假设你有一个 tagsView 标签页栏)

// 打开标签页时
store.commit('tagsView/ADD_CACHED_VIEW', {
  name: to.matched[0].components.default.name
})

// 关闭标签页时
store.commit('tagsView/DEL_CACHED_VIEW', {
  name: closedTab.name
})

Step 4:AppMain.vue 中的缓存核心逻辑




重点说明:

  • 使用 :include="cachedViews" 来控制缓存组件;
  • :key="$route.fullPath" 让 Vue 判断页面是否需要重渲;
  • 分别处理 keepAlive: true 和 false 的路由逻辑。

Step 5:刷新页面的正确姿势(强制卸载 + 重建)

methods: {
  refreshView(view) {
    this.$store.commit('tagsView/DEL_CACHED_VIEW', view)
    this.$nextTick(() => {
      this.$router.replace({ path: '/redirect' + view.fullPath }) // redirect 路由统一刷新
    })
  }
}

总结:Vue2 缓存调度核心五件套

组件 功能
name 缓存组件必须声明
meta.keepAlive 判断是否需要缓存
Vuex cachedViews 管理缓存白名单
keep-alive :include 控制缓存行为
key="$route.fullPath" 区分路由组件状态

下一节我们将对比:Vue3 的缓存机制到底优化了哪些点?是不是更“智能”、更“好用”?
我们将从原理差异到开发实践,对比 Vue2 与 Vue3 的缓存实现差异,揭示 Vue3 对痛点的改良,也为后续的升级迁移打下基础。


四、Vue3 的缓存机制进化:更现代、更透明、更易控

1. 概览:keep-alive 在 Vue3 中的变化

先来一张总结图:

对比项 Vue2 Vue3
使用方式 (PascalCase 推荐)
匹配依据 component.name 同样依赖 name,但增强支持动态组件
生命周期 activated / deactivated 同名钩子,支持 Composition API
缓存控制 include/exclude 同样支持,但更加灵活
类型支持 较弱 强类型(TS 支持更友好)
动态缓存更新 手动维护 支持组合式响应式控制
缓存粒度 组件级 同样组件级,未来配合 signal 有望更细粒度

一句话总结:Vue3 并没有“重写” keep-alive,但它带来了更强的组合式可控性,真正掌控缓存行为不再是 hack,而是内建能力。


2. 生命周期:从 Options API 到 Composition API

Vue2 的生命周期钩子:

export default {
  activated() {
    console.log('组件被激活')
  },
  deactivated() {
    console.log('组件被缓存')
  }
}

Vue3 完全支持上述写法,同时新增组合式 API 支持:

import { onActivated, onDeactivated } from 'vue'

setup() {
  onActivated(() => {
    console.log('组件被激活')
  })
  onDeactivated(() => {
    console.log('组件被缓存')
  })
}

优势:逻辑归位、状态可组合、逻辑可抽离(自定义 hooks)


3. 缓存控制的“现代方式”:响应式 include 列表

Vue3 的 KeepAlive 支持绑定一个响应式的数组作为 include,动态增删更自然:


  
    
  

import { ref } from 'vue'
const cachedNames = ref(['Dashboard', 'Settings'])

function addToCache(name) {
  if (!cachedNames.value.includes(name)) {
    cachedNames.value.push(name)
  }
}

function removeFromCache(name) {
  cachedNames.value = cachedNames.value.filter(n => n !== name)
}

Vue3 的响应式缓存列表结合 Composition API,可以封装为 useCacheManager() composable,彻底告别 Vuex 操作。


4. 更精准的组件识别:setup 组件也支持缓存

Vue3 修复了一个 Vue2 的老问题:

在 Vue2 中,匿名组件或者 setup 返回的匿名组件可能无法被缓存。

Vue3 明确要求组件传入 name,并可以配合

极大提升了缓存准确率,尤其在 SFC 单文件组件中。


5. Vue3 + Suspense + Teleport,缓存扩展玩法更多

虽然不直接 related,但 Vue3 的组合能力为缓存机制开辟了新空间:

  • 在异步组件加载中包裹 KeepAlive,避免加载抖动;
  • Teleport 结合保持 modal/cache 状态;
  • 未来 Vue 官方计划引入信号式响应式(Signals),组件缓存将更精细化。

小结:Vue2 vs Vue3 缓存机制对比图谱

特性 Vue2 Vue3
缓存控制方式 include/exclude include/exclude(响应式增强)
生命周期钩子 activated / deactivated 同名 + 组合式支持
组件识别方式 name name(setup 支持更好)
动态缓存列表 Vuex 手动维护 可组合响应式维护
支持 async setup
更高扩展能力 ⛔️ ✅ 支持 Teleport/Suspense 等周边结合

总结

  • Vue3 沿用了 Vue2 的缓存机制设计理念,但提供了 更现代、更易组合、更类型安全的实现方式
  • 如果你正在维护 Vue2 项目,可以借鉴 Vue3 的结构,预做组合式迁移准备;
  • 如果你计划升级 Vue3,可以将缓存控制逻辑封装为 Composables,彻底剥离原始 Vuex 和 mutation 写法。

你可能感兴趣的:(一次 “简单需求“ 引发的 Vue 缓存血案:从 keep-alive 踩坑到 Vue3 优化全记录)