关于 <KeepAlive>,看这篇文章就够了!
原文重点分析 Vue 2
<keep-alive>的cache、keys、include、exclude和max。这些原理仍有参考价值,但源码片段经过网页抓取后已经严重拼接,且 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 中移出或进入隐藏状态,但不会立即按普通卸载流程销毁。


二、三个常用参数
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 对应 activated、deactivated、unmounted/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,而不是立即销毁。include和exclude按组件 name 过滤,max限制缓存数量并进行 LRU 淘汰。Vue 2 的实现重点是cache + keys + componentInstance,Vue 3 仍保留相同的缓存语义但 renderer 和内部实现不同。Vue Router 4/5 中应通过 RouterView slot 缓存实际路由组件。KeepAlive 是内存和初始化成本之间的取舍,不是无条件的性能优化。
参考资料
- Vue 2 KeepAlive API
- Vue 2 动态组件与异步组件
- Vue 2.7 KeepAlive 源码
- Vue 3 KeepAlive
- Vue 3 KeepAlive API
- Vue Router RouterView slot
原文作者:剑侠客。原文的动态组件示例、缓存参数、Vue 2 cache/keys/LRU 分析予以保留,抓取噪声和混淆 Vue 2/Vue 3 的源码表述已清理。