跳到主要内容

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>
匹配规则

includeexclude 匹配的是组件的 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 的核心原理:

  1. 缓存 VNode:使用 Map 存储组件的 VNode 和实例
  2. 复用实例:再次渲染时,直接复用缓存的组件实例
  3. LRU 淘汰:设置 max 后,超出限制会删除最久未使用的
  4. 特殊标记:通过 shapeFlag 标记组件为 keep-alive 类型
  5. 生命周期:卸载时不销毁,而是调用 deactivated;恢复时调用 activated

Q2: activated 和 mounted 哪个先执行?

答案

  • 首次渲染mountedactivated
  • 从缓存恢复:只触发 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-alivev-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>

相关链接