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

显示模式

登录
ARCHIVE DOCUMENTVUE

关于 <KeepAlive>,看这篇文章就够了!

所属馆藏
Vue
文件格式
Markdown
原始路径
Vue/65-关于keep-alive,看这篇文章就够了!
本文目录9 个章节
  1. 一、为什么需要 KeepAlive
  2. 二、三个常用参数
  3. 三、Vue 2.7 的实现原理
  4. 四、Vue 3.5 的 KeepAlive
  5. 五、Vue Router 4/5 中缓存路由组件
  6. 六、KeepAlive 的生命周期和数据刷新
  7. 七、常见误区
  8. 八、面试版回答
  9. 参考资料

关于 <KeepAlive>,看这篇文章就够了!

原文重点分析 Vue 2 <keep-alive>cachekeysincludeexcludemax。这些原理仍有参考价值,但源码片段经过网页抓取后已经严重拼接,且 Vue 3 的 KeepAlive 实现、RouterView 用法和生命周期名称不同。本文保留历史源码思路,并补充 Vue 3.5 / Vue Router 5 的写法。

一、为什么需要 KeepAlive

动态组件默认切换时会卸载旧组件、创建新组件:

<script setup lang="ts">
import { ref } from 'vue'
import ChildOne from './ChildOne.vue'
import ChildTwo from './ChildTwo.vue'

const current = ref<'ChildOne' | 'ChildTwo'>('ChildOne')
</script>

<template>
  <button @click="current = 'ChildOne'">组件 1</button>
  <button @click="current = 'ChildTwo'">组件 2</button>

  <component :is="current === 'ChildOne' ? ChildOne : ChildTwo" />
</template>

如果两个子组件都有 input,切换回来后通常会重新创建实例,之前的输入状态消失。使用 <KeepAlive> 可以缓存非活动组件实例:

<KeepAlive>
  <component :is="current === 'ChildOne' ? ChildOne : ChildTwo" />
</KeepAlive>

缓存的是组件实例及其渲染状态,不是简单把一段 HTML 字符串存起来。非活动组件会从 DOM 中移出或进入隐藏状态,但不会立即按普通卸载流程销毁。

动态组件切换的历史示意图(已本地化)

KeepAlive 缓存输入状态的历史示意图(已本地化)

二、三个常用参数

1. include

只缓存名称匹配的组件:

<KeepAlive include="ChildOne,ChildTwo">
  <component :is="currentComponent" />
</KeepAlive>

也可以传数组或正则:

<KeepAlive :include="['ChildOne', 'ChildTwo']">
  <component :is="currentComponent" />
</KeepAlive>

<KeepAlive :include="/ChildOne|ChildTwo/">
  <component :is="currentComponent" />
</KeepAlive>

2. exclude

匹配到的组件不缓存:

<KeepAlive :exclude="['LoginView']">
  <component :is="currentComponent" />
</KeepAlive>

3. max

限制缓存实例数量:

<KeepAlive :max="5">
  <component :is="currentComponent" />
</KeepAlive>

达到上限后,最久未使用的缓存项会被淘汰。max 是内存控制和用户体验的取舍,不是越大越好。

include/exclude 匹配的是组件 name。Vue 2 通常依赖组件的 name 选项;Vue 3 SFC 会根据文件名推断 name,也可以显式声明:

<script setup lang="ts">
defineOptions({ name: 'UserList' })
</script>

如果名称没有匹配,组件可能不会按预期缓存。组件 name 还用于 DevTools 和递归自引用,不等于路由 name 或 VNode key。

三、Vue 2.7 的实现原理

Vue 2 的 <keep-alive> 是 abstract component,不渲染额外的真实 DOM。源码中有两个重要结构:

this.cache = Object.create(null)
this.keys = []
  • cache[key] 保存缓存的 VNode;
  • keys 保存缓存 key 的使用顺序;
  • 命中缓存时取回 componentInstance
  • 命中后把 key 移到 keys 末尾;
  • 超过 max 时淘汰最早的 key。

Vue 2 的 render 思路可以简化为:

export default {
  name: 'keep-alive',
  abstract: true,
  props: {
    include: [String, RegExp, Array],
    exclude: [String, RegExp, Array],
    max: [String, Number],
  },

  created() {
    this.cache = Object.create(null)
    this.keys = []
  },

  render() {
    const slot = this.$slots.default
    const vnode = getFirstComponentChild(slot)
    const options = vnode && vnode.componentOptions

    if (!options) return vnode

    const name = getComponentName(options)
    if (
      (this.include && (!name || !matches(this.include, name))) ||
      (this.exclude && name && matches(this.exclude, name))
    ) {
      return vnode
    }

    const key = vnode.key == null
      ? options.Ctor.cid + (options.tag ? `::${options.tag}` : '')
      : vnode.key

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

      if (this.max && this.keys.length > Number(this.max)) {
        pruneCacheEntry(this.cache, this.keys[0], this.keys, this._vnode)
      }
    }

    vnode.data.keepAlive = true
    return vnode
  },
}

上面是经过整理的源码结构,不是可以直接替换 Vue 2.7 内部实现的完整源码。真实 renderer 还要在组件创建、激活、插入和销毁阶段处理 keepAlive 标记。

3.1 命中缓存时发生了什么

Vue 2 组件创建流程中,如果发现 VNode 已有 componentInstance 且带有 keepAlive 标记,会复用缓存的组件实例和 DOM 引用,并把它重新插入父节点:

function createComponent(vnode, parentElm, refElm) {
  const isReactivated = vnode.componentInstance && vnode.data.keepAlive

  if (!vnode.componentInstance) {
    vnode.componentInstance = createComponentInstance(vnode)
    vnode.componentInstance.$mount(undefined, false)
  }

  insert(parentElm, vnode.elm, refElm)

  if (isReactivated) {
    activateComponent(vnode)
  }

  return true
}

原文中的 insert(parentElm, vnode.elm, refElm) 体现的是“取回并插入缓存 DOM”的核心思路,但真实源码还包含 hooks、transition、父子组件和 hydration 分支。

3.2 LRU 淘汰和清理

Vue 2 的 keys 维护最近使用顺序,清理逻辑大致如下:

function pruneCacheEntry(cache, key, keys, current) {
  const cached = cache[key]

  if (cached && (!current || cached.tag !== current.tag)) {
    cached.componentInstance.$destroy()
  }

  cache[key] = null
  remove(keys, key)
}

如果动态修改 include/exclude,KeepAlive 会遍历缓存,把不再匹配的组件销毁并移除。max 淘汰也必须销毁实例,否则只从数组中删除 key 会造成资源泄漏。

Vue 2 组件销毁时还会清理所有缓存:

destroyed() {
  for (const key in this.cache) {
    pruneCacheEntry(this.cache, key, this.keys)
  }
}

四、Vue 3.5 的 KeepAlive

Vue 3 使用内置组件 <KeepAlive>,基本用法相同:

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

Vue 3 的组件可以使用 Composition API 生命周期:

<script setup lang="ts">
import { onActivated, onDeactivated, onUnmounted } from 'vue'

let timer: number | undefined

onActivated(() => {
  timer = window.setInterval(() => {
    // 组件重新显示时恢复轮询
  }, 10_000)
})

onDeactivated(() => {
  if (timer !== undefined) window.clearInterval(timer)
  timer = undefined
})

onUnmounted(() => {
  if (timer !== undefined) window.clearInterval(timer)
})
</script>

Options API 对应 activateddeactivatedunmounted/destroyed 等版本相关钩子。被 KeepAlive 缓存的组件切换出去时触发 deactivated,不是立即 unmounted;真正被淘汰或父组件卸载时才会进入销毁流程。

五、Vue Router 4/5 中缓存路由组件

不要直接把 <KeepAlive> 放在 <RouterView> 外层包裹 RouterView 本身。应使用 RouterView 的 slot,缓存实际路由组件:

<RouterView v-slot="{ Component }">
  <KeepAlive :include="cachedNames" :max="10">
    <component :is="Component" />
  </KeepAlive>
</RouterView>

如果还要使用过渡,可以组合:

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

Vue Router 3/Vue 2 项目常见旧写法:

<keep-alive>
  <router-view />
</keep-alive>

旧写法在历史项目中可以继续维护,但迁移到 Vue Router 4/5 时,推荐使用 RouterView slot,避免缓存 RouterView 包装组件而不是实际路由组件。

如果希望同一路由不同参数拥有不同缓存实例,需要显式设置 key:

<RouterView v-slot="{ Component, route }">
  <KeepAlive>
    <component :is="Component" :key="route.fullPath" />
  </KeepAlive>
</RouterView>

是否使用 fullPath 要根据需求决定:它可能让 query 变化创建更多缓存项,增加内存压力。没有 key 时,组件可能复用同一个实例,然后通过 watch route 参数刷新数据。

六、KeepAlive 的生命周期和数据刷新

普通卸载组件的生命周期大致是:

beforeUnmount → unmounted

缓存组件切换出去时更接近:

deactivated

重新切回来时:

activated

因此:

  • onMounted 只适合一次性初始化;
  • onActivated 适合恢复轮询、刷新可见页面数据;
  • onDeactivated 适合暂停轮询、移除临时监听;
  • onUnmounted 仍然要做最终资源清理;
  • 不要在 activated 中无条件重复注册全局事件而不清理。
<script setup lang="ts">
import { onActivated, onDeactivated } from 'vue'

function handleVisibilityChange() {
  // 根据当前页面可见性刷新数据
}

onActivated(() => {
  document.addEventListener('visibilitychange', handleVisibilityChange)
})

onDeactivated(() => {
  document.removeEventListener('visibilitychange', handleVisibilityChange)
})
</script>

KeepAlive 不会自动重新请求数据,也不会自动解决后端数据过期问题。应明确缓存时长、刷新策略和失效条件。

七、常见误区

误区 1:KeepAlive 缓存的是 HTML

它缓存的是组件实例及 VNode/renderer 管理的状态,DOM 只是其中一部分结果。

误区 2:缓存组件切换出去就销毁

通常会触发 deactivated,不是立即卸载。只有被淘汰或父级卸载时才真正销毁。

误区 3:KeepAlive 一定提升性能

它减少重复创建和初始化,但会占用内存,缓存过多页面会导致内存增长、旧数据展示和后台资源持续存在。max、include/exclude 和主动刷新策略需要一起设计。

误区 4:所有路由都应该缓存

登录页、一次性表单、实时数据页和内存占用大的编辑器不一定适合缓存。缓存应以用户返回时是否需要保留状态为依据。

误区 5:只要设置 key 就能解决所有问题

key 决定 VNode/组件身份。不同 key 会创建不同缓存项,也可能快速消耗 max;它不能替代数据刷新、资源清理和缓存失效策略。

八、面试版回答

<KeepAlive> 是 Vue 的内置抽象组件,用来缓存动态组件实例。组件第一次渲染时创建并放入缓存,切换回来时复用实例;切换出去通常触发 activated/deactivated,而不是立即销毁。includeexclude 按组件 name 过滤,max 限制缓存数量并进行 LRU 淘汰。Vue 2 的实现重点是 cache + keys + componentInstance,Vue 3 仍保留相同的缓存语义但 renderer 和内部实现不同。Vue Router 4/5 中应通过 RouterView slot 缓存实际路由组件。KeepAlive 是内存和初始化成本之间的取舍,不是无条件的性能优化。

参考资料

原文作者:剑侠客。原文的动态组件示例、缓存参数、Vue 2 cache/keys/LRU 分析予以保留,抓取噪声和混淆 Vue 2/Vue 3 的源码表述已清理。

457 DOCUMENTS · 10 COLLECTIONS
ARCHIVE SEARCH457 篇文章

SEARCH GUIDE

输入关键词开始搜索

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

按分类浏览

10 COLLECTIONS