Vue 中 keep-alive 组件的原理与实践详解
在 Vue 开发中,我们经常会遇到这样的场景:列表页筛选数据后进入详情页,返回列表页时筛选状态却丢失了------这是因为默认情况下,Vue 会销毁不活动的组件,重新进入时会创建新实例。而 keep-alive 组件正是为解决这类问题而生,它能缓存不活动的组件实例,避免重复渲染,提升用户体验。本文将从核心概念、使用方式、源码解析到渲染机制,全面拆解 keep-alive 的工作原理。
一、keep-alive 核心概念
keep-alive 是 Vue 内置的全局抽象组件,具有以下关键特性:
- 自身不会渲染为真实 DOM,也不会出现在父组件链中
- 包裹动态组件或路由组件时,会缓存不活动的组件实例(而非销毁)
- 支持通过配置参数控制缓存范围和数量上限
- 本质是通过缓存组件的虚拟 DOM(VNode),实现组件状态的持久化
二、keep-alive 基本使用
keep-alive 接收三个核心 props 参数,支持灵活控制缓存行为:
| 参数 | 类型 | 说明 |
|---|---|---|
| include | 字符串/正则/数组 | 名称匹配的组件会被缓存 |
| exclude | 字符串/正则/数组 | 名称匹配的组件不会被缓存 |
| max | 字符串/数字 | 缓存组件的最大数量,超出按 LRU 算法置换 |
2.1 包裹动态组件
vue
<keep-alive :include="allowList" :exclude="noAllowList" :max="5">
<component :is="currentComponent"></component>
</keep-alive>
2.2 包裹路由组件
vue
<keep-alive :include="['List', 'Detail']" :max="3">
<router-view></router-view>
</keep-alive>
注意:
include和exclude匹配的是组件的name选项,而非组件文件名或标签名。
三、keep-alive 源码深度解析
keep-alive 的核心逻辑集中在 src/core/components/keep-alive.js 中,我们从组件结构、生命周期、核心函数三方面展开分析。
3.1 组件基础结构
javascript
export default {
name: 'keep-alive',
abstract: true, // 关键:标记为抽象组件,不参与DOM渲染
props: {
include: patternTypes, // 允许缓存的组件匹配规则
exclude: patternTypes, // 禁止缓存的组件匹配规则
max: [String, Number] // 缓存数量上限
},
// 生命周期与核心方法...
}
abstract: true 是 keep-alive 不渲染自身的关键:Vue 在初始化生命周期时,会跳过抽象组件,不将其加入父组件链(见 src/core/instance/lifecycle.js)。
3.2 生命周期钩子
3.2.1 created:初始化缓存容器
javascript
created() {
this.cache = Object.create(null) // 存储缓存的VNode(虚拟DOM)
this.keys = [] // 存储缓存组件的key,用于LRU排序
}
cache:对象结构,key 为组件唯一标识,value 为组件的 VNode 实例keys:数组结构,维护缓存组件的访问顺序,支持 LRU 算法
3.2.2 mounted:监听缓存规则变化
javascript
mounted() {
// 监听include变化,更新缓存(保留匹配的组件)
this.$watch('include', val => {
pruneCache(this, name => matches(val, name))
})
// 监听exclude变化,更新缓存(移除匹配的组件)
this.$watch('exclude', val => {
pruneCache(this, name => !matches(val, name))
})
}
通过监听 include 和 exclude 的变化,动态清理不符合规则的缓存组件。
3.2.3 destroyed:清理所有缓存
javascript
destroyed() {
// 遍历缓存,销毁所有组件实例并清空缓存
for (const key in this.cache) {
pruneCacheEntry(this.cache, key, this.keys)
}
}
3.3 核心工具函数
3.3.1 pruneCacheEntry:销毁单个缓存项
javascript
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) // 从key列表中移除
}
作用:销毁组件实例、释放内存、更新缓存列表,是缓存清理的核心方法。
3.3.2 render:缓存逻辑核心
keep-alive 的核心逻辑都在 render 函数中,负责组件的缓存判断、存储和更新,步骤如下:
- 获取目标组件:获取包裹的第一个子组件的 VNode 和组件配置
javascript
const slot = this.$slots.default
const vnode = getFirstComponentChild(slot) // 仅处理第一个子组件
const componentOptions = vnode && vnode.componentOptions
- 缓存规则校验 :判断组件是否符合
include/exclude规则
javascript
const name = getComponentName(componentOptions) // 获取组件name
const { include, exclude } = this
// 不满足include 或 命中exclude,直接返回VNode,不缓存
if ((include && !matches(include, name)) || (exclude && matches(exclude, name))) {
return vnode
}
- 生成缓存key:基于组件唯一标识生成key(避免重复缓存)
javascript
const key = vnode.key || `${componentOptions.Ctor.cid}::${componentOptions.tag}`
- 优先使用组件自身的
key - 无
key时,用「组件构造函数ID + 标签名」拼接
- 缓存命中处理:若组件已缓存,更新访问顺序(LRU核心)
javascript
if (cache[key]) {
// 复用缓存的组件实例
vnode.componentInstance = cache[key].componentInstance
// 移除原有key,添加到数组末尾(标记为最近访问)
remove(keys, key)
keys.push(key)
}
- 缓存未命中处理:新增缓存,检查数量上限
javascript
else {
cache[key] = vnode // 缓存VNode
keys.push(key)
// 超过max限制,删除最久未访问的缓存(LRU置换)
if (this.max && keys.length > parseInt(this.max)) {
pruneCacheEntry(cache, keys[0], keys, this._vnode)
}
}
- 标记缓存状态 :设置
keepAlive标识,供后续渲染使用
javascript
vnode.data.keepAlive = true
四、keep-alive 渲染机制
Vue 组件的常规渲染流程为:render -> VNode -> patch -> 真实DOM,而 keep-alive 包裹的组件会在这个流程中加入缓存判断,核心在于 patch 阶段的 createComponent 函数。
4.1 常规组件渲染流程
new Vue → init → $mount → compile → render → VNode → patch → 真实DOM
- 初次渲染:
patch直接将 VNode 转为真实 DOM - 后续渲染:
patch通过 diff 算法对比新旧 VNode,打补丁更新 DOM
4.2 keep-alive 渲染特殊处理
keep-alive 包裹的组件渲染逻辑在 src/core/vdom/patch.js 的 createComponent 函数中:
javascript
function createComponent(vnode, insertedVnodeQueue, parentElm, refElm) {
const i = vnode.data
if (isDef(i)) {
// 判断是否为缓存组件(vnode.data.keepAlive为true)
const isReactivated = isDef(vnode.componentInstance) && i.keepAlive
if (isDef(i = i.hook) && isDef(i = i.init)) {
i(vnode, false) // 初次渲染时初始化组件
}
// 若为缓存组件,直接复用DOM
if (isDef(vnode.componentInstance)) {
initComponent(vnode, insertedVnodeQueue)
// 直接插入缓存的真实DOM(vnode.elm),无需重新渲染
insert(parentElm, vnode.elm, refElm)
if (isTrue(isReactivated)) {
reactivateComponent(vnode, insertedVnodeQueue, parentElm, refElm)
}
return true
}
}
}
关键逻辑区分:
- 初次访问 :
vnode.componentInstance为undefined,执行init钩子初始化组件,生成 DOM 并缓存 - 再次访问 :
vnode.componentInstance已存在(缓存的实例),直接调用insert插入缓存的 DOM,跳过组件初始化和渲染过程
五、核心总结
keep-alive 的本质是基于 VNode 的缓存管理组件,核心设计思路可概括为:
- 抽象组件特性 :通过
abstract: true实现自身不渲染,仅作为缓存容器 - 缓存存储结构 :用
cache对象存储 VNode,keys数组维护访问顺序 - LRU 置换算法 :超过
max时删除keys数组头部(最久未访问)的缓存项 - 渲染优化逻辑:复用缓存的 VNode 和组件实例,跳过初始化和 diff 过程,直接插入真实 DOM
- 动态缓存控制 :通过监听
include/exclude变化,实时清理无效缓存
合理使用 keep-alive 能显著提升页面切换性能(尤其是列表页、表单页等需要保留状态的场景),但需注意避免无限制缓存过多组件,以免占用过多内存。
🎯 2025年最新大厂前端场景面试题(完整题库+解析)
一份系统整理的前端专项面试资料,内容涵盖 JavaScript、React、Vue、Node.js、性能优化、工程化、浏览器原理、网络协议 等高频考点,所有题目均来自大厂真实场景与近期面试趋势。
📚 资料详情
方向:前端开发
格式:PDF
页数:251页(含详细答案与扩展解析)
适用:面试冲刺、体系复盘、技术查漏
📥 获取方式
👉 夸克网盘链接:https://pan.quark.cn/s/bcf695cf5ede
(可直接下载,无需提取码)
希望这份详细的题库能助你高效备战,顺利拿到心仪的 Offer!加油