keep-alive 原理
问题
Vue 的 keep-alive 是什么?它是如何实现组件缓存的?
答案
<keep-alive> 是 Vue 内置的抽象组件,用于缓存动态组件,避免重复渲染,保留组件状态。
基本用法
<template>
<!-- 缓存动态组件 -->
<keep-alive>
<component :is="currentComponent" />
</keep-alive>
<!-- 配合 v-if 使用 -->
<keep-alive>
<CompA v-if="show" />
<CompB v-else />
</keep-alive>
<!-- 配合 vue-router -->
<router-view v-slot="{ Component }">
<keep-alive>
<component :is="Component" />
</keep-alive>
</router-view>
</template>
keep-alive 属性
<template>
<!-- include:只缓存匹配的组件 -->
<keep-alive include="CompA,CompB">
<component :is="current" />
</keep-alive>
<!-- 正则表达式 -->
<keep-alive :include="/^Comp/">
<component :is="current" />
</keep-alive>
<!-- 数组 -->
<keep-alive :include="['CompA', 'CompB']">
<component :is="current" />
</keep-alive>
<!-- exclude:排除匹配的组件 -->
<keep-alive exclude="CompC">
<component :is="current" />
</keep-alive>
<!-- max:最大缓存数量(LRU 策略) -->
<keep-alive :max="10">
<component :is="current" />
</keep-alive>
</template>
匹配规则
include 和 exclude 匹配的是组件的 name 选项:
// Options API
export default {
name: 'CompA'
}
// script setup(需要单独定义)
defineOptions({
name: 'CompA'
})
生命周期钩子
被 keep-alive 缓存的组件有两个专属生命周期:
<script setup lang="ts">
import { onActivated, onDeactivated } from 'vue';
// 组件被激活时(从缓存中恢复)
onActivated(() => {
console.log('组件激活');
// 刷新数据、恢复滚动位置等
});
// 组件被停用时(进入缓存)
onDeactivated(() => {
console.log('组件停用');
// 保存状态、清理定时器等
});
</script>
实现原理
核心数据结构
// keep-alive 组件内部
const cache = new Map<CacheKey, VNode>(); // 缓存的 VNode
const keys = new Set<CacheKey>(); // 缓存的 key 集合
interface KeepAliveContext {
cache: Map<CacheKey, VNode>;
keys: Set<CacheKey>;
max: number;
}
缓存逻辑
// 简化版实现
const KeepAlive = {
name: 'KeepAlive',
setup(props, { slots }) {
const cache = new Map();
const keys = new Set();
let current: VNode | null = null;
// 卸载时清空缓存
onUnmounted(() => {
cache.forEach((vnode) => {
// 销毁缓存的组件实例
unmount(vnode);
});
});
return () => {
// 获取默认插槽的第一个子节点
const children = slots.default?.();
const vnode = children?.[0];
if (!vnode) return null;
const key = vnode.key ?? vnode.type;
const cachedVNode = cache.get(key);
if (cachedVNode) {
// 命中缓存:复用组件实例
vnode.component = cachedVNode.component;
// 标记为 keep-alive 组件
vnode.shapeFlag |= ShapeFlags.COMPONENT_KEPT_ALIVE;
// LRU:更新 key 的位置
keys.delete(key);
keys.add(key);
} else {
// 未命中:加入缓存
cache.set(key, vnode);
keys.add(key);
// 超出 max,删除最久未使用的
if (props.max && keys.size > props.max) {
const oldestKey = keys.values().next().value;
pruneCacheEntry(cache, oldestKey);
keys.delete(oldestKey);
}
}
// 标记为 keep-alive 组件
vnode.shapeFlag |= ShapeFlags.COMPONENT_SHOULD_KEEP_ALIVE;
current = vnode;
return vnode;
};
}
};
LRU 缓存策略
keep-alive 使用 LRU(Least Recently Used) 算法管理缓存:
// LRU 实现:使用 Set 保持插入顺序
const keys = new Set<CacheKey>();
// 访问/添加元素
function access(key: CacheKey) {
if (keys.has(key)) {
// 已存在:删除后重新添加(移到末尾)
keys.delete(key);
}
keys.add(key);
}
// 淘汰最久未使用的
function evict() {
const oldest = keys.values().next().value;
keys.delete(oldest);
cache.delete(oldest);
}
激活/停用原理
// 渲染器处理 keep-alive 组件
function processComponent(n1: VNode | null, n2: VNode, container: Element) {
if (n2.shapeFlag & ShapeFlags.COMPONENT_KEPT_ALIVE) {
// 从缓存恢复,调用 activated
const instance = n2.component!;
move(n2, container);
// 触发 activated 钩子
instance.a?.forEach(hook => hook());
return;
}
// 正常挂载...
}
function unmount(vnode: VNode) {
if (vnode.shapeFlag & ShapeFlags.COMPONENT_SHOULD_KEEP_ALIVE) {
// 不真正卸载,而是调用 deactivated
const instance = vnode.component!;
// 触发 deactivated 钩子
instance.da?.forEach(hook => hook());
// 移动到隐藏容器
move(vnode, storageContainer);
return;
}
// 正常卸载...
}
使用场景
<!-- 1. 多 Tab 切换 -->
<template>
<div class="tabs">
<button v-for="tab in tabs" @click="currentTab = tab">
{{ tab }}
</button>
</div>
<keep-alive>
<component :is="currentTabComponent" />
</keep-alive>
</template>
<!-- 2. 列表 -> 详情 -> 返回列表 -->
<template>
<router-view v-slot="{ Component }">
<keep-alive include="ListView">
<component :is="Component" />
</keep-alive>
</router-view>
</template>
<!-- 3. 条件缓存 -->
<template>
<router-view v-slot="{ Component, route }">
<keep-alive v-if="route.meta.keepAlive">
<component :is="Component" />
</keep-alive>
<component v-else :is="Component" />
</router-view>
</template>
<script setup lang="ts">
// router 配置
const routes = [
{
path: '/list',
component: ListView,
meta: { keepAlive: true }
},
{
path: '/detail/:id',
component: DetailView
}
];
</script>
常见面试问题
Q1: keep-alive 的原理是什么?
答案:
keep-alive 的核心原理:
- 缓存 VNode:使用
Map存储组件的 VNode 和实例 - 复用实例:再次渲染时,直接复用缓存的组件实例
- LRU 淘汰:设置
max后,超出限制会删除最久未使用的 - 特殊标记:通过
shapeFlag标记组件为 keep-alive 类型 - 生命周期:卸载时不销毁,而是调用
deactivated;恢复时调用activated
Q2: activated 和 mounted 哪个先执行?
答案:
- 首次渲染:
mounted→activated - 从缓存恢复:只触发
activated(不会再触发mounted)
// 首次渲染
setup() → onBeforeMount() → onMounted() → onActivated()
// 缓存后再次激活
onActivated() // 只触发这一个
// 进入缓存
onDeactivated()
Q3: keep-alive 如何清除缓存?
答案:
<template>
<!-- 方法1:动态 include -->
<keep-alive :include="cachedComponents">
<router-view />
</keep-alive>
</template>
<script setup lang="ts">
import { ref } from 'vue';
const cachedComponents = ref(['CompA', 'CompB']);
// 清除特定组件的缓存
function clearCache(name: string) {
const index = cachedComponents.value.indexOf(name);
if (index > -1) {
cachedComponents.value.splice(index, 1);
// 下次进入会重新渲染
}
}
// 方法2:通过 key 强制刷新
const routerKey = ref(0);
function forceRefresh() {
routerKey.value++;
}
</script>
<template>
<router-view :key="routerKey" />
</template>
Q4: keep-alive 会导致什么问题?
答案:
| 问题 | 原因 | 解决方案 |
|---|---|---|
| 数据不刷新 | 组件被缓存,created/mounted 不再执行 | 在 onActivated 中刷新数据 |
| 内存占用 | 缓存太多组件 | 设置 max 属性 |
| 状态残留 | 表单数据等状态保留 | 手动重置或清除缓存 |
| 生命周期混乱 | 不理解 activated/deactivated | 正确使用 keep-alive 钩子 |
// 解决数据不刷新
onActivated(() => {
// 每次激活时检查是否需要刷新
if (shouldRefresh()) {
fetchData();
}
});
// 解决状态残留
onDeactivated(() => {
// 清理临时状态
formData.value = {};
});
Q5: keep-alive 和 v-show 有什么区别?
答案:
| 特性 | keep-alive | v-show |
|---|---|---|
| 原理 | 缓存虚拟 DOM 和组件实例 | CSS display: none |
| DOM 操作 | 组件 DOM 真正移除/恢复 | DOM 始终存在 |
| 生命周期 | activated/deactivated | 无 |
| 适用场景 | 动态组件、路由缓存 | 简单元素切换 |
| 初始渲染 | 首次才挂载 | 始终渲染 |
<!-- v-show:DOM 始终存在,只切换 display -->
<div v-show="visible">内容</div>
<!-- keep-alive:组件实例被缓存,DOM 真正移除 -->
<keep-alive>
<CompA v-if="showA" />
<CompB v-else />
</keep-alive>