技术知识文章集合TECHNICAL ARCHIVE · 457 DOCUMENTS

显示模式

登录
ARCHIVE DOCUMENTVUE

给我五分钟,我把 KeepAlive 的用法和原理跟你说明白

所属馆藏
Vue
文件格式
Markdown
原始路径
Vue/94-给我五分钟,我把 keep-alive 的用法和原理跟你说明白
本文目录8 个章节
  1. 一、KeepAlive 解决什么问题
  2. 二、完整的动态组件示例
  3. 三、和 Vue Router 一起缓存页面
  4. 四、缓存组件的生命周期
  5. 五、KeepAlive 的实现原理
  6. 六、常见问题与排查顺序
  7. 总结
  8. 参考资料

给我五分钟,我把 KeepAlive 的用法和原理跟你说明白

原文主要介绍 Vue 2 的 <keep-alive>、路由页面缓存和 LRU 原理。本文保留原有示例和源码学习路线,去掉抓取产生的代码标记,补充 Vue 3.5、Vue Router 4/5、<router-view> 插槽、组件名称匹配和缓存失效场景。

一、KeepAlive 解决什么问题

默认情况下,动态组件切换会卸载当前组件实例,再创建下一个组件实例:

<component :is="activeComponent" />

如果组件中有输入框、分页、滚动位置或临时筛选条件,离开后再回来通常会重新初始化。Vue 内置的 <KeepAlive> 可以缓存动态组件实例:组件从页面移除时进入 deactivated 状态,而不是立即卸载;再次显示时从缓存中 activated

<KeepAlive>
  <component :is="activeComponent" />
</KeepAlive>

它缓存的是组件实例及其子树的运行时状态,不是把页面 HTML 永久保存下来,也不是浏览器 HTTP 缓存。

没有 KeepAlive 时切换页面的示例(已本地化)

KeepAlive 的三个常用属性

属性作用
include只缓存名称匹配的组件
exclude排除名称匹配的组件
max限制缓存实例数量,超出后淘汰最久未访问的实例

includeexclude 支持逗号分隔字符串、正则表达式和数组:

<KeepAlive include="Home,UserList">
  <component :is="activeComponent" />
</KeepAlive>

<KeepAlive :include="['Home', 'UserList']">
  <component :is="activeComponent" />
</KeepAlive>

<KeepAlive :exclude="/Edit/">
  <component :is="activeComponent" />
</KeepAlive>

匹配的是组件的 name,不是路由配置里的 name 字段。需要按名称筛选时,应确保组件拥有正确的 name

<script setup>
defineOptions({ name: 'Home' })
</script>

Vue 3.2.34 及以后,使用 <script setup> 的单文件组件通常会根据文件名自动推断组件名;为了避免重命名文件、打包工具或异步组件造成歧义,重要的缓存组件仍可以显式声明 name

max 和 LRU

<KeepAlive :max="10">
  <component :is="activeComponent" />
</KeepAlive>

当缓存数量即将超过 max 时,KeepAlive 会淘汰最近最少使用(Least Recently Used,LRU)的缓存实例。被淘汰的实例会真正卸载,相关的 onUnmounted/unmounted 清理逻辑会执行。

max 不是“最多渲染 10 个组件”,而是“最多保留 10 个已缓存的组件实例”。缓存项数量、组件子树大小和页面数据都会影响内存占用。

二、完整的动态组件示例

Home.vue

<script setup>
import { ref } from 'vue'

defineOptions({ name: 'Home' })

const keyword = ref('')
</script>

<template>
  <section>
    <h1>首页</h1>
    <input v-model="keyword" placeholder="输入内容后切换页面" />
    <p>当前输入:{{ keyword }}</p>
  </section>
</template>

About.vue

<script setup>
defineOptions({ name: 'About' })
</script>

<template>
  <section>
    <h1>关于页面</h1>
  </section>
</template>

App.vue

<script setup>
import { ref } from 'vue'
import Home from './Home.vue'
import About from './About.vue'

const activeComponent = ref(Home)
</script>

<template>
  <nav>
    <button @click="activeComponent = Home">Home</button>
    <button @click="activeComponent = About">About</button>
  </nav>

  <KeepAlive include="Home">
    <component :is="activeComponent" />
  </KeepAlive>
</template>

只有 Home 会被缓存。切换到 About 时,Home 进入 deactivated;再次切回时,输入框仍然保留原来的值。若移除 include,两个组件都会进入缓存。

Vue 2 的等价写法:

<keep-alive include="Home">
  <component :is="activeComponent" />
</keep-alive>

组件需要使用 export default { name: 'Home' } 声明名称。Vue 2 的组件名匹配规则和 Vue 3 基本一致,但 Vue 3 的 <script setup> 自动推断能力不能直接套用到 Vue 2 项目。

三、和 Vue Router 一起缓存页面

1. Vue 2 / Router 3 的常见写法

历史项目中,直接包裹 <router-view> 是常见方案:

<template>
  <div id="app">
    <keep-alive include="Home">
      <router-view />
    </keep-alive>
  </div>
</template>

路由组件:

// router.js,Vue 2 + vue-router 3
import Vue from 'vue'
import Router from 'vue-router'
import Home from './views/Home.vue'
import About from './views/About.vue'

Vue.use(Router)

export default new Router({
  routes: [
    {
      path: '/',
      name: 'home',
      component: Home,
    },
    {
      path: '/about',
      name: 'about',
      component: () => import('./views/About.vue'),
    },
  ],
})

include="Home" 匹配的是 Home.vue 的组件 name,不是上面路由记录的 name: 'home'。两者大小写也可能不同,排查缓存不生效时应先检查这一点。

2. Vue 3 / Router 4–5 的推荐写法

Vue Router 4/5 提供了 <router-view> 插槽。KeepAlive 应该放在插槽内部,缓存真正的路由组件,而不是缓存 RouterView 组件本身:

<template>
  <RouterView v-slot="{ Component }">
    <KeepAlive include="Home">
      <component :is="Component" />
    </KeepAlive>
  </RouterView>
</template>

如果还要配合过渡动画:

<template>
  <RouterView v-slot="{ Component }">
    <Transition mode="out-in">
      <KeepAlive>
        <component :is="Component" />
      </KeepAlive>
    </Transition>
  </RouterView>
</template>

TransitionKeepAlive 的嵌套顺序应根据期望的进入/离开效果验证,不要只复制某个版本的写法。

3. 按路由 meta 决定是否缓存

可以在路由记录中保存业务配置:

// router/index.ts,Vue Router 4/5
const routes = [
  {
    path: '/',
    name: 'home',
    component: () => import('../views/Home.vue'),
    meta: { keepAlive: true },
  },
  {
    path: '/about',
    name: 'about',
    component: () => import('../views/About.vue'),
    meta: { keepAlive: false },
  },
]

模板可以按当前路由分支:

<template>
  <RouterView v-slot="{ Component, route }">
    <KeepAlive>
      <component
        v-if="route.meta.keepAlive"
        :is="Component"
        :key="route.meta.cacheKey || route.name"
      />
    </KeepAlive>

    <component
      v-if="!route.meta.keepAlive"
      :is="Component"
      :key="route.fullPath"
    />
  </RouterView>
</template>

实际项目中要保证同一次渲染只有一个 route component 作为 KeepAlive 的子节点,并根据参数路由决定 key 策略。beforeRouteLeave 中给 to.meta.keepAlive 赋值的旧示例容易污染路由记录,也不能覆盖所有返回路径,通常不如在路由配置中明确声明。

路由页面使用 KeepAlive 的示例(已本地化)

4. 参数路由的 key

/users/1/users/2 可能复用同一个 User 组件实例,因为路由组件类型没有改变:

<RouterView v-slot="{ Component, route }">
  <KeepAlive>
    <!-- 需要每个参数一份缓存时才使用 fullPath -->
    <component :is="Component" :key="route.fullPath" />
  </KeepAlive>
</RouterView>

使用 fullPath 会让每个参数组合都成为一个缓存项,可能快速占满缓存。若希望两个用户共用组件实例并监听 route.params.id,可以不加这个 key,并在组件中响应参数变化。

四、缓存组件的生命周期

Options API:

export default {
  activated() {
    // 首次挂载时也会调用,之后每次从缓存重新插入时调用
    this.refreshIfNeeded()
  },

  deactivated() {
    // 从 DOM 移入缓存时调用;组件真正卸载时也会调用
    this.stopPolling()
  },
}

Composition API:

<script setup>
import { onActivated, onDeactivated, onUnmounted } from 'vue'

let timer

function startPolling() {
  timer = window.setInterval(() => {
    // 拉取或刷新当前页面需要的数据
  }, 30_000)
}

function stopPolling() {
  window.clearInterval(timer)
  timer = undefined
}

onActivated(startPolling)
onDeactivated(stopPolling)
onUnmounted(stopPolling)
</script>

需要注意:

  • activated/onActivated 首次挂载时也会执行;
  • deactivated 不是 unmounted,组件实例、DOM 状态和本地响应式数据仍在缓存中;
  • KeepAlive 不应被理解为自动暂停所有定时器、WebSocket、第三方订阅或业务 watcher;这些外部资源需要在生命周期中自行管理;
  • 组件最终因 KeepAlive 淘汰或父树卸载时,仍会执行卸载清理。

使用 KeepAlive 后保留输入状态(已本地化)

五、KeepAlive 的实现原理

1. 它不是普通的 DOM 容器

KeepAlive 是 Vue 的内置组件。它不会额外渲染一个 <div>,而是接收默认插槽中的动态组件 vnode,并告诉 renderer:

  • 这个 vnode 可以进入缓存;
  • 再次出现时复用原来的组件实例;
  • 离开时执行 deactivate,而不是普通 unmount;
  • 需要淘汰时才执行真正的卸载。

Vue 2 中它是抽象组件,核心源码位于 src/core/components/keep-alive.js;Vue 3 中实现位于 runtime-core 的 KeepAlive 组件,并与 renderer 的 activate/deactivate 流程协作。

2. Vue 2 源码的核心模型

下面是从 Vue 2 源码抽取的教学版伪代码,省略了异步组件、transition 和销毁细节:

const cache = Object.create(null)
const keys = []

function render(vnode, include, exclude, max) {
  const name = getComponentName(vnode)

  if (
    (include && (!name || !matches(include, name))) ||
    (exclude && name && matches(exclude, name))
  ) {
    return vnode
  }

  const key = vnode.key == null
    ? `${vnode.componentOptions.Ctor.cid}::${vnode.componentOptions.tag || ''}`
    : vnode.key

  if (cache[key]) {
    vnode.componentInstance = cache[key].componentInstance
    remove(keys, key)
    keys.push(key)
  } else {
    cache[key] = vnode
    keys.push(key)

    if (max && keys.length > Number(max)) {
      const oldestKey = keys.shift()
      pruneCacheEntry(cache, oldestKey)
    }
  }

  vnode.data.keepAlive = true
  return vnode
}

真实源码还会在 include/exclude 改变时清理不符合条件的缓存,并在组件销毁时遍历缓存销毁实例。上面 cache/keys 的关系体现了原文所说的 LRU:命中缓存时把 key 移到末尾,淘汰时移除最前面的 key。

3. Vue 3 的实现重点

Vue 3 仍然是同一套思想,但运行时 vnode 有 KeepAlive 专用标记,renderer 会把组件子树移动到内部存储容器:

首次出现:mount component
切换离开:deactivate,保留 instance 和 vnode
再次出现:activate,把已有 DOM 插回目标位置
超过 max:prune cache,unmount 被淘汰实例

实现中会使用缓存表和 key 集合记录组件,并根据 includeexcludemax 处理缓存。不同小版本的内部字段和优化可能变化,不应在业务代码中直接访问 KeepAlive 的内部 cache

KeepAlive 缓存和淘汰流程(已本地化)

六、常见问题与排查顺序

1. 为什么写了 KeepAlive 仍然不缓存

按以下顺序检查:

  1. KeepAlive 的子节点是否真的是动态组件或 RouterView slot 中的路由组件;
  2. include/exclude 是否匹配组件 name,大小写是否正确;
  3. 是否给每次切换都设置了变化的 key,导致每次都是新缓存项;
  4. 组件是否被 v-if 完全移出 KeepAlive 外部;
  5. max 是否太小,导致组件刚离开就被淘汰;
  6. 是否误以为 KeepAlive 会缓存 Pinia、接口数据或浏览器滚动位置——这些需要单独处理。

2. 什么时候不应该使用

不适合无限缓存大型页面、包含大量图表/编辑器的实例,或每次进入都必须重新获取权限与数据的页面。可以使用 max、手动刷新策略、分页、状态管理或服务端缓存控制内存与数据新鲜度。

3. 如何清除缓存

KeepAlive 没有一个推荐给业务代码直接调用的统一 clearCache() 公共 API。常用方式是:

  • 调整 include,让目标组件不再匹配;
  • 修改合理的 vnode key,使其创建新实例;
  • 通过条件渲染让 KeepAlive 整体卸载再重新挂载;
  • 将需要持久化的数据移到 Pinia 等状态层,而不是依赖组件缓存。

这些方式会影响组件生命周期,应结合业务确认。

总结

  • <KeepAlive> 缓存的是动态组件实例,不是普通 HTML 或接口响应;
  • include/exclude 按组件 name 匹配,max 使用 LRU 思路淘汰旧实例;
  • 缓存组件离开时触发 deactivated,再次出现时触发 activated;
  • 路由缓存中,Vue Router 4/5 推荐把 KeepAlive 放到 <router-view> 插槽内部;
  • Vue 2 可以直接包裹 <router-view>,但应区分 Router 3 与 Router 4/5 的写法;
  • 参数路由的 key 决定是复用同一实例,还是为每个参数创建独立缓存;
  • 缓存不是越多越好,应处理定时器、WebSocket、内存和数据刷新问题。

参考资料

原文作者:白瓷梅子汤。原文关于动态组件缓存、includeexcludemaxactivated/deactivated、路由缓存和 LRU 的主线予以保留;抓取的代码标记、Vue 2/Router 3 写法与 Vue 3 混淆、错误的组件名说明以及不严谨的缓存清理表述已整理或修正。

457 DOCUMENTS · 10 COLLECTIONS
ARCHIVE SEARCH457 篇文章

SEARCH GUIDE

输入关键词开始搜索

支持搜索文章标题、所属分类和原始文档路径。

按分类浏览

10 COLLECTIONS