Appearance
✨ keep-alive 生命周期 👌
要点速览
- 功能:缓存组件实例,避免频繁销毁/重建,保留内部状态与数据。
- 核心钩子:
onActivated(激活)与onDeactivated(失活);不再重复触发mounted/unmounted。 - 缓存控制:
include/exclude/max精准控制缓存范围与数量(超额触发最近最少使用淘汰)。 - 适用场景:Tab/切换视图、复杂表单向导、路由组件缓存(与
router-view配合)。 - 注意:失活期间无真实 DOM,避免访问;在激活时刷新数据或恢复副作用。
概念与类比
keep-alive 借鉴了 HTTP 的“持久连接”思想:避免重复销毁与重建,提升性能。在 Vue 中它用于对组件进行缓存,从而复用组件实例与其内部状态。
基本使用
未缓存的切换会频繁创建/销毁组件:
vue
<template>
<Tab v-if="currentTab === 1" />
<Tab v-if="currentTab === 2" />
<Tab v-if="currentTab === 3" />
<!-- 切换时会卸载旧 Tab 并重建新 Tab -->
<!-- 导致 mounted/unmounted 反复触发,状态重置 -->
<!-- 性能在大组件场景下尤为明显 -->
</template>加入缓存后,切换不再销毁组件实例:
vue
<template>
<keep-alive>
<Tab v-if="currentTab === 1" />
<Tab v-if="currentTab === 2" />
<Tab v-if="currentTab === 3" />
</keep-alive>
</template>缓存控制与匹配规则
include:指定需要缓存的组件。支持字符串、正则、数组;依据组件的name选项匹配。exclude:排除不缓存的组件。max:最大缓存数量;超出时淘汰最久未使用的实例(LRU)。
示例:
vue
<template>
<keep-alive :include="['UserList', 'UserDetail']" :max="5">
<component :is="currentView" />
</keep-alive>
</template>建议:为可缓存的组件显式设置 name,确保匹配稳定;需要多份独立缓存时使用不同的 key。
生命周期与时序
时序详解(未缓存 vs 缓存)
未使用 keep-alive:切换组件 A → B 的一次完整时序
- A
beforeUnmount(准备卸载,仍可访问实例) - B
created(创建阶段,状态就绪但未挂载) - B
beforeMount(首次渲染前) - A
unmounted(A 的 DOM 与渲染副作用已清理) - B
mounted(B 首次插入 DOM,可安全访问真实节点)
- A
使用 keep-alive:切换与缓存的行为差异
- 首次进入某视图:与未缓存一致,正常触发该视图的
mounted。 - 之后在已缓存视图之间切换:
- 离开当前视图 A:触发
onDeactivated(A 被移出 DOM,但实例与状态保留) - 进入目标视图 B:触发
onActivated(B 被插入 DOM,恢复渲染与副作用)
- 离开当前视图 A:触发
- 由于实例未销毁,
mounted/unmounted不会重复触发;只有当缓存被淘汰或显式清理时才会重新触发卸载/挂载。
- 首次进入某视图:与未缓存一致,正常触发该视图的
何时会重新触发
mounted/unmounted- 超过
max触发 LRU 淘汰,被淘汰的实例将执行unmounted;再次进入时会重新mounted。 - 通过条件渲染移除缓存容器或修改
include/exclude使实例不再命中缓存。
- 超过
典型场景代码:
vue
<script setup>
import { onActivated, onDeactivated } from "vue";
let timer = null;
onActivated(() => {
// 恢复副作用(例如重启轮询/订阅)
timer = setInterval(() => {
/* ... */
}, 2000);
});
onDeactivated(() => {
// 暂停副作用(组件失活期间不在 DOM 中)
clearInterval(timer);
timer = null;
});
</script>常见误区与建议
- 误区:期望每次进入都执行
mounted。建议:使用onActivated作为进入点,onDeactivated作为离开点。 - 误区:失活期间访问真实 DOM。建议:在激活时再访问/更新 DOM;失活期间组件不在文档流中。
- 误区:未为组件设置
name导致include/exclude无效。建议:显式设置name并与匹配值一致。 - 误区:缓存过多导致内存占用。建议:合理设置
max,并在离开页面时清理不再需要的缓存。 - 建议:对订阅、计时器、第三方实例在
onActivated/onDeactivated做暂停与恢复,避免后台空转。
与路由的结合
常见做法是基于路由 meta.keepAlive 与组件 name 控制缓存;并通过 key 决定是否区分参数/查询的不同实例。
方式一:按 meta 决定是否缓存
vue
<template>
<router-view v-slot="{ Component, route }">
<keep-alive>
<component
v-if="route.meta.keepAlive"
:is="Component"
:key="route.name"
/>
</keep-alive>
<component v-else :is="Component" :key="route.name" />
</router-view>
<!-- route.name 复用同名组件的缓存;若需区分不同参数,改为 route.fullPath -->
</template>方式二:使用 include/exclude 精确匹配组件名
vue
<template>
<router-view v-slot="{ Component, route }">
<keep-alive :include="cachedNames" :max="5">
<component :is="Component" :key="route.name" />
</keep-alive>
</router-view>
</template>
<script setup>
import { ref } from "vue";
// 与路由配置中的组件 name 保持一致
const cachedNames = ref(["UserList", "UserDetail"]);
</script>实践要点:
- 为路由组件显式设置
name,以便与include/exclude匹配。 :key="route.name"复用同名缓存;:key="route.fullPath"为不同参数创建独立缓存。- 使用
max控制缓存规模;超额将执行 LRU 淘汰并触发unmounted。 - 仅在需要保留复杂状态的视图使用缓存;避免对轻量页面滥用缓存导致内存占用。
注意:确保路由组件具备稳定的 name;若需要区分同名组件的不同实例,使用不同的 key(如基于路由参数)。
示例
vue
<template>
<div>
<button @click="currentTab = 1">Tab1</button>
<button @click="currentTab = 2">Tab2</button>
<button @click="currentTab = 3">Tab3</button>
<keep-alive :include="['TabOne', 'TabTwo']" :max="2">
<component :is="viewComp" />
</keep-alive>
</div>
</template>
<script setup>
import { computed, ref, defineComponent, h } from "vue";
const TabOne = defineComponent({
name: "TabOne",
setup() {
const count = ref(0);
const add = () => {
count.value++;
};
return () =>
h("div", null, [
h("h3", null, "Tab 1"),
h("button", { onClick: add }, `Count: ${count.value}`),
]);
},
});
const TabTwo = defineComponent({
name: "TabTwo",
setup() {
const count = ref(0);
const add = () => {
count.value++;
};
return () =>
h("div", null, [
h("h3", null, "Tab 2"),
h("button", { onClick: add }, `Count: ${count.value}`),
]);
},
});
const TabThree = defineComponent({
name: "TabThree",
setup() {
const count = ref(0);
const add = () => {
count.value++;
};
return () =>
h("div", null, [
h("h3", null, "Tab 3"),
h("button", { onClick: add }, `Count: ${count.value}`),
]);
},
});
const currentTab = ref(1);
const viewComp = computed(() =>
currentTab.value === 1 ? TabOne : currentTab.value === 2 ? TabTwo : TabThree
);
</script>