Vue3 KeepAlive 深入解析
大约 7 分钟约 2068 字
Vue3 KeepAlive 深入解析
什么是 KeepAlive?
Vue3 的 <KeepAlive> 是一个内置组件,用于缓存动态组件或路由组件的实例。当组件被切换走时,它不会被销毁,而是进入"停用"状态保存在内存中。当再次切换回来时,组件会从缓存中恢复到"激活"状态,保留了之前的状态(如表单数据、滚动位置、请求结果等)。
KeepAlive 的核心价值:
- 保持状态 — 切换页面时表单数据不丢失,用户不会因为误操作丢失填写内容
- 提升体验 — 切换回来的页面无需重新请求和渲染,瞬间恢复
- 节省资源 — 避免重复创建和销毁组件的开销
适用场景:
- 多标签页导航(类似浏览器的 Tab)
- 列表页 → 详情页 → 返回列表页(保持滚动位置和筛选条件)
- 复杂表单页面切换(保持已填写的表单数据)
- 数据监控大屏(保持轮询状态)
基础用法
动态组件缓存
<template>
<div class="app-container">
<nav>
<button
v-for="tab in tabs"
:key="tab.id"
:class="{ active: currentTab === tab.id }"
@click="currentTab = tab.id"
>
{{ tab.label }}
</button>
</nav>
<!-- 使用 KeepAlive 缓存动态组件 -->
<KeepAlive>
<component :is="tabComponent" :key="currentTab" />
</KeepAlive>
</div>
</template>
<script setup lang="ts">
import { ref, shallowRef, markRaw } from 'vue';
import DeviceMonitor from './DeviceMonitor.vue';
import AlarmHistory from './AlarmHistory.vue';
import DataAnalysis from './DataAnalysis.vue';
const tabs = [
{ id: 'monitor', label: '设备监控', component: markRaw(DeviceMonitor) },
{ id: 'alarm', label: '告警历史', component: markRaw(AlarmHistory) },
{ id: 'analysis', label: '数据分析', component: markRaw(DataAnalysis) },
];
const currentTab = ref('monitor');
const tabComponent = shallowRef(DeviceMonitor);
// 切换 Tab
function switchTab(tabId: string) {
currentTab.value = tabId;
tabComponent.value = tabs.find(t => t.id === tabId)!.component;
}
</script>include 和 exclude 属性
<template>
<!-- 方式 1:字符串(逗号分隔) -->
<KeepAlive include="DeviceMonitor,AlarmHistory">
<component :is="currentView" />
</KeepAlive>
<!-- 方式 2:正则表达式 -->
<KeepAlive :include="/^(Device|Alarm)/">
<component :is="currentView" />
</KeepAlive>
<!-- 方式 3:数组 -->
<KeepAlive :include="['DeviceMonitor', 'AlarmHistory', 'Dashboard']">
<component :is="currentView" />
</KeepAlive>
<!-- 排除特定组件 -->
<KeepAlive :exclude="['Login', 'Register']">
<component :is="currentView" />
</KeepAlive>
<!-- 同时使用 include 和 exclude -->
<KeepAlive :include="/^(Device|Alarm)/" :exclude="['DeviceDetail']">
<component :is="currentView" />
</KeepAlive>
</template>max 属性 — LRU 缓存淘汰
<template>
<!-- 最多缓存 10 个组件实例 -->
<!-- 超出时,最久未访问的实例会被销毁 -->
<KeepAlive :max="10">
<component :is="currentView" />
</KeepAlive>
</template>LRU(Least Recently Used)淘汰策略:当缓存数量超过 max 时,最久没有被激活的组件实例会被销毁。例如缓存了 A、B、C,用户依次访问 A → B → C → D,由于 max 为 3,A 会被销毁。
路由级缓存
基础路由缓存
<!-- App.vue -->
<template>
<RouterView v-slot="{ Component, route }">
<KeepAlive :include="cachedViews">
<component :is="Component" :key="route.fullPath" />
</KeepAlive>
</RouterView>
</template>
<script setup lang="ts">
import { ref } from 'vue';
const cachedViews = ref<string[]>(['Dashboard', 'DeviceMonitor', 'AlarmHistory']);
</script>通过路由 meta 动态管理
// router/index.ts
import { createRouter, createWebHistory } from 'vue-router';
const router = createRouter({
history: createWebHistory(),
routes: [
{
path: '/dashboard',
name: 'Dashboard',
component: () => import('@/views/Dashboard.vue'),
meta: { title: '仪表盘', keepAlive: true },
},
{
path: '/devices',
name: 'DeviceMonitor',
component: () => import('@/views/DeviceMonitor.vue'),
meta: { title: '设备监控', keepAlive: true },
},
{
path: '/devices/:id',
name: 'DeviceDetail',
component: () => import('@/views/DeviceDetail.vue'),
meta: { title: '设备详情', keepAlive: false }, // 详情页不缓存
},
{
path: '/alarms',
name: 'AlarmHistory',
component: () => import('@/views/AlarmHistory.vue'),
meta: { title: '告警历史', keepAlive: true },
},
{
path: '/login',
name: 'Login',
component: () => import('@/views/Login.vue'),
meta: { title: '登录', keepAlive: false },
},
],
});
export default router;<!-- App.vue — 动态计算缓存列表 -->
<template>
<RouterView v-slot="{ Component, route }">
<KeepAlive :include="cachedViews">
<component :is="Component" :key="route.fullPath" />
</KeepAlive>
</RouterView>
</template>
<script setup lang="ts">
import { ref, watch } from 'vue';
import { useRouter } from 'vue-router';
const router = useRouter();
const cachedViews = ref<string[]>([]);
// 监听路由变化,动态管理缓存列表
watch(
() => router.currentRoute.value,
(to) => {
if (to.meta.keepAlive && to.name && !cachedViews.value.includes(to.name as string)) {
cachedViews.value.push(to.name as string);
}
},
{ immediate: true }
);
// 手动添加缓存
function addCachedView(viewName: string) {
if (!cachedViews.value.includes(viewName)) {
cachedViews.value.push(viewName);
}
}
// 手动移除缓存
function removeCachedView(viewName: string) {
const index = cachedViews.value.indexOf(viewName);
if (index > -1) {
cachedViews.value.splice(index, 1);
}
}
// 清除所有缓存
function clearAllCachedViews() {
cachedViews.value = [];
}
// 暴露方法给其他组件使用
defineExpose({ addCachedView, removeCachedView, clearAllCachedViews });
</script>相同路由不同参数
<template>
<!-- 问题:/devices/1 和 /devices/2 使用同一个组件实例 -->
<!-- 解决:使用 route.fullPath 作为 key -->
<RouterView v-slot="{ Component, route }">
<KeepAlive :include="cachedViews">
<component :is="Component" :key="route.fullPath" />
</KeepAlive>
</RouterView>
</template><!-- DeviceDetail.vue — 监听路由参数变化 -->
<script setup lang="ts">
import { watch, onActivated } from 'vue';
import { useRoute } from 'vue-router';
const route = useRoute();
// 方式 1:watch 路由参数
watch(
() => route.params.id,
(newId) => {
loadDeviceDetail(newId as string);
}
);
// 方式 2:在 activated 中获取最新参数
onActivated(() => {
const deviceId = route.params.id as string;
loadDeviceDetail(deviceId);
});
</script>缓存组件的生命周期
activated 和 deactivated
被 KeepAlive 包裹的组件有两个额外的生命周期钩子:
// DeviceMonitor.vue
<script setup lang="ts">
import { onActivated, onDeactivated, onMounted, onUnmounted } from 'vue';
// 组件首次创建时调用
onMounted(() => {
console.log('组件首次挂载');
startPolling(); // 开始数据轮询
connectWebSocket(); // 建立 WebSocket 连接
});
// 组件从缓存中恢复(每次激活都调用)
onActivated(() => {
console.log('组件被激活(从缓存恢复)');
startPolling(); // 恢复轮询
connectWebSocket(); // 重新连接 WebSocket
refreshData(); // 刷新可能过期的数据
});
// 组件进入缓存(每次停用都调用)
onDeactivated(() => {
console.log('组件被停用(进入缓存)');
stopPolling(); // 停止轮询,节省资源
disconnectWebSocket(); // 断开 WebSocket
});
// 组件真正销毁时调用(从缓存中清除或页面关闭)
onUnmounted(() => {
console.log('组件被销毁');
cleanup();
});
</script>生命周期执行顺序
首次渲染:
setup() → onMounted() → activated()
切换走(进入缓存):
deactivated()
切换回来(从缓存恢复):
activated()
从缓存中清除(max 超出或手动清除):
deactivated() → onUnmounted()多标签页导航实现
Tabs + KeepAlive 组合
<!-- MainLayout.vue -->
<template>
<div class="main-layout">
<!-- 顶部导航 -->
<header class="layout-header">
<div class="logo">管理后台</div>
<nav class="nav-menu">
<router-link to="/dashboard">首页</router-link>
<router-link to="/devices">设备</router-link>
<router-link to="/alarms">告警</router-link>
</nav>
</header>
<!-- 标签栏 -->
<div class="tabs-bar">
<div
v-for="tab in openTabs"
:key="tab.path"
class="tab-item"
:class="{ active: currentPath === tab.path }"
@click="switchTab(tab)"
>
<span>{{ tab.title }}</span>
<span v-if="tab.closable" class="tab-close" @click.stop="closeTab(tab)">x</span>
</div>
</div>
<!-- 内容区域 -->
<main class="layout-content">
<RouterView v-slot="{ Component, route }">
<KeepAlive :include="cachedViews">
<component :is="Component" :key="route.fullPath" />
</KeepAlive>
</RouterView>
</main>
</div>
</template>
<script setup lang="ts">
import { ref, computed, watch } from 'vue';
import { useRouter, useRoute } from 'vue-router';
interface TabItem {
path: string;
name: string;
title: string;
closable: boolean;
}
const router = useRouter();
const route = useRoute();
const openTabs = ref<TabItem[]>([
{ path: '/dashboard', name: 'Dashboard', title: '首页', closable: false },
]);
const currentPath = computed(() => route.path);
const cachedViews = computed(() =>
openTabs.value
.filter(tab => tab.name)
.map(tab => tab.name)
);
// 监听路由变化,自动添加 Tab
watch(
() => route.path,
() => {
if (!openTabs.value.find(t => t.path === route.path)) {
openTabs.value.push({
path: route.path,
name: route.name as string,
title: (route.meta.title as string) || route.path,
closable: route.path !== '/dashboard', // 首页不可关闭
});
}
},
{ immediate: true }
);
// 切换 Tab
function switchTab(tab: TabItem) {
router.push(tab.path);
}
// 关闭 Tab
function closeTab(tab: TabItem) {
const index = openTabs.value.findIndex(t => t.path === tab.path);
if (index === -1) return;
openTabs.value.splice(index, 1);
// 如果关闭的是当前 Tab,切换到上一个
if (tab.path === currentPath.value) {
const nextTab = openTabs.value[Math.min(index, openTabs.value.length - 1)];
router.push(nextTab.path);
}
}
</script>
<style scoped>
.tabs-bar {
display: flex;
gap: 4px;
padding: 8px 16px;
background: #f5f5f5;
border-bottom: 1px solid #e8e8e8;
}
.tab-item {
display: flex;
align-items: center;
gap: 8px;
padding: 4px 12px;
background: #fff;
border: 1px solid #e8e8e8;
border-radius: 4px;
cursor: pointer;
font-size: 13px;
}
.tab-item.active {
background: #1890ff;
color: #fff;
border-color: #1890ff;
}
.tab-close {
font-size: 12px;
opacity: 0.7;
}
.tab-close:hover {
opacity: 1;
}
</style>常见陷阱
陷阱 1:忘记清理定时器和监听器
// Bad — 定时器在停用后仍在运行
onMounted(() => {
setInterval(() => {
fetchData();
}, 5000);
});
// Good — 在 deactivated 中清理
let timer: number | null = null;
onMounted(() => {
timer = window.setInterval(fetchData, 5000);
});
onActivated(() => {
if (!timer) timer = window.setInterval(fetchData, 5000);
});
onDeactivated(() => {
if (timer) { clearInterval(timer); timer = null; }
});
onUnmounted(() => {
if (timer) { clearInterval(timer); timer = null; }
});陷阱 2:组件 name 与 include 不匹配
// Vue3 <script setup> 默认没有 name 属性
// 需要通过以下方式设置:
// 方式 1:使用 vite-plugin-vue-setup-extend 插件
// <script setup lang="ts" name="DeviceMonitor">
// 方式 2:单独的 script 标签
<script lang="ts">
export default { name: 'DeviceMonitor' };
</script>
<script setup lang="ts">
// 组件逻辑
</script>
// 方式 3:使用 defineOptions(Vue 3.3+)
<script setup lang="ts">
defineOptions({ name: 'DeviceMonitor' });
</script>陷阱 3:缓存过多导致内存泄漏
<!-- 设置合理的 max 值 -->
<KeepAlive :max="15">
<component :is="currentView" />
</KeepAlive>
<!-- 定期检查内存使用 -->
<!-- Chrome DevTools → Memory → Take Heap Snapshot -->性能考虑
- 内存占用:每个缓存实例都占用内存(DOM、状态、事件监听器),max 值不宜过大
- 数据过期:缓存的数据可能已过时,在 onActivated 中刷新
- 初次渲染:KeepAlive 不影响初次渲染性能,只在切换时有优势
- max 推荐:10-20 个,根据业务复杂度调整
最佳实践总结
- 在路由 meta 中统一标记 keepAlive — 通过 meta.keepAlive 动态管理缓存列表
- 设置合理的 max 值 — 防止内存持续增长
- 处理 activated/deactivated — 在停用时清理副作用,在激活时恢复
- 设置组件 name — Vue3 的
<script setup>需要手动设置 name - 相同路由不同参数用 fullPath 做 key — 避免复用同一个缓存实例
- 在 onActivated 中刷新数据 — 避免展示过期的缓存数据
