一、这东西是什么
Vue KeepAlive 是 Vue.js 的内置组件,用于缓存不活动的组件实例,而不是销毁它们。当组件在 <KeepAlive> 内切换时,它的状态会被保留,避免重复渲染。
核心功能:
- 组件缓存:保存组件实例,避免重复创建
- 状态保持:保持组件数据和 DOM 状态
- 生命周期扩展:新增
activated和deactivated钩子 - 智能缓存:基于 LRU 算法管理缓存大小
二、这东西有什么用
适用场景
- 标签页切换(如后台管理系统)
- 列表页到详情页的返回
- 表单填写中途离开
- 需要保持滚动位置的页面
- 性能敏感的应用(减少渲染开销)
能带来什么收益
- 性能提升:减少组件创建和销毁的开销
- 用户体验:保持页面状态,避免数据丢失
- 流畅切换:快速切换,无闪烁
- 内存管理:智能缓存,避免内存泄漏
三、官方链接
四、从源码看实现原理
1. KeepAlive 组件结构(Vue 3 源码)
// vue-next/packages/runtime-core/src/components/KeepAlive.ts
export const KeepAliveImpl: ComponentOptions = {
name: `KeepAlive`,
// 组件属性
props: {
include: [String, RegExp, Array],
exclude: [String, RegExp, Array],
max: [String, Number]
},
setup(props: KeepAliveProps, { slots }: SetupContext) {
// 缓存映射:key -> 组件实例
const cache: Map<CacheKey, VNode> = new Map()
// 键的访问顺序(用于 LRU)
const keys: Set<CacheKey> = new Set()
// 当前渲染的组件
let current: VNode | null = null
// 父组件实例
const parentSuspense = getCurrentInstance()!.suspense
// 共享上下文
const sharedContext = getCurrentInstance()!.ctx
// 缓存组件
function cacheSubtree() {
if (current) {
cache.set(current.key!, current)
keys.add(current.key!)
}
}
// 激活组件
function activate(vnode: VNode, container: RendererElement, anchor: RendererNode | null) {
// 调用组件的 activated 钩子
if (vnode.shapeFlag & ShapeFlags.COMPONENT_SHOULD_KEEP_ALIVE) {
vnode.component!.activated(vnode, container, anchor)
}
}
// 失活组件
function deactivate(vnode: VNode) {
// 调用组件的 deactivated 钩子
if (vnode.shapeFlag & ShapeFlags.COMPONENT_SHOULD_KEEP_ALIVE) {
vnode.component!.deactivated(vnode)
}
}
// 渲染函数
return () => {
// 获取默认插槽内容
const children = slots.default?.()
if (!children || !children.length) {
return null
}
const rawVNode = children[0]
// 只缓存有 key 的组件
if (rawVNode.key == null) {
return rawVNode
}
// 检查 include/exclude
const name = getComponentName(rawVNode.type)
if (
name &&
((props.include && !matches(props.include, name)) ||
(props.exclude && matches(props.exclude, name)))
) {
return rawVNode
}
const key = rawVNode.key!
const cachedVNode = cache.get(key)
// 缓存命中
if (cachedVNode) {
// 更新访问顺序(LRU)
keys.delete(key)
keys.add(key)
// 标记为已缓存
rawVNode.el = cachedVNode.el
rawVNode.component = cachedVNode.component
// 标记需要保持激活状态
rawVNode.shapeFlag |= ShapeFlags.COMPONENT_KEPT_ALIVE
} else {
// 缓存未命中,添加到缓存
keys.add(key)
// 检查缓存是否超过最大限制
if (props.max && keys.size > parseInt(props.max as string)) {
// LRU:移除最久未使用的
const keyToDelete = keys.values().next().value
keys.delete(keyToDelete)
cache.delete(keyToDelete)
}
cache.set(key, rawVNode)
}
// 标记为需要保持激活
rawVNode.shapeFlag |= ShapeFlags.COMPONENT_SHOULD_KEEP_ALIVE
current = rawVNode
return rawVNode
}
}
}
2. LRU 缓存算法实现
// LRU(Least Recently Used)缓存实现
class LRUCache<K, V> {
private capacity: number
private cache: Map<K, V>
constructor(capacity: number) {
this.capacity = capacity
this.cache = new Map()
}
get(key: K): V | undefined {
if (!this.cache.has(key)) {
return undefined
}
// 访问时移动到最新位置
const value = this.cache.get(key)!
this.cache.delete(key)
this.cache.set(key, value)
return value
}
put(key: K, value: V): void {
if (this.cache.has(key)) {
this.cache.delete(key)
} else if (this.cache.size >= this.capacity) {
// 删除最久未使用的(第一个)
const firstKey = this.cache.keys().next().value
this.cache.delete(firstKey)
}
this.cache.set(key, value)
}
}
五、如何做一个 demo 出来
1. 环境要求
- Node.js 14+
- Vue 3.0+
- Vite 或 Vue CLI
2. 安装命令
# 创建 Vue 3 项目
npm create vue@latest vue-keepalive-demo
# 选择配置
# ✔ Project name: vue-keepalive-demo
# ✔ Add TypeScript? Yes
# ✔ Add JSX Support? No
# ✔ Add Vue Router for Single Page Application development? Yes
# ✔ Add Pinia for state management? Yes
# ✔ Add Vitest for Unit Testing? No
# ✔ Add an End-to-End Testing Solution? No
# ✔ Add ESLint for code quality? Yes
cd vue-keepalive-demo
npm install
3. 目录结构说明
vue-keepalive-demo/
├── src/
│ ├── components/
│ │ ├── TabContent/ # 标签页内容组件
│ │ ├── UserList/ # 用户列表组件
│ │ └── UserDetail/ # 用户详情组件
│ ├── views/
│ │ ├── Home.vue # 首页
│ │ ├── About.vue # 关于页
│ │ └── KeepAliveDemo.vue # KeepAlive 演示页
│ ├── router/
│ │ └── index.ts # 路由配置
│ ├── App.vue
│ └── main.ts
├── package.json
└── vite.config.ts
4. 最小可运行示例
基础使用:
<!-- src/views/KeepAliveDemo.vue -->
<template>
<div class="keepalive-demo">
<h2>KeepAlive 基础演示</h2>
<!-- 切换按钮 -->
<div class="tabs">
<button
v-for="tab in tabs"
:key="tab.name"
:class="{ active: currentTab === tab.name }"
@click="currentTab = tab.name"
{{ tab.label }}
</button>
</div>
<!-- KeepAlive 包裹动态组件 -->
<KeepAlive>
<component :is="currentComponent" :key="currentTab" />
</KeepAlive>
<!-- ���示缓存状态 -->
<div class="cache-info">
<h3>缓存信息</h3>
<p>当前标签: {{ currentTab }}</p>
<p>缓存组件: {{ cachedComponents.join(', ') }}</p>
</div>
</div>
</template>
<script setup lang="ts">
import { ref, computed, shallowRef } from 'vue'
import TabContent from '@/components/TabContent.vue'
// 标签配置
const tabs = [
{ name: 'tab1', label: '标签页 1', component: TabContent },
{ name: 'tab2', label: '标签页 2', component: TabContent },
{ name: 'tab3', label: '标签页 3', component: TabContent }
]
// 当前选中的标签
const currentTab = ref('tab1')
// 当前组件
const currentComponent = computed(() => {
const tab = tabs.find(t => t.name === currentTab.value)
return tab?.component || TabContent
})
// 模拟缓存状态
const cachedComponents = ref<string[]>(['tab1'])
</script>
<style scoped>
.keepalive-demo {
padding: 20px;
}
.tabs {
margin-bottom: 20px;
}
.tabs button {
padding: 10px 20px;
margin-right: 10px;
border: 1px solid #ddd;
background: white;
cursor: pointer;
}
.tabs button.active {
background: #1890ff;
color: white;
border-color: #1890ff;
}
.cache-info {
margin-top: 30px;
padding: 15px;
background: #f5f5f5;
border-radius: 4px;
}
</style>
带生命周期的组件:
<!-- src/components/TabContent.vue -->
<template>
<div class="tab-content">
<h3>{{ title }}</h3>
<!-- 计数器演示状态保持 -->
<div class="counter">
<p>计数: {{ count }}</p>
<button @click="count++">增加</button>
<button @click="count = 0">重置</button>
</div>
<!-- 输入框演示状态保持 -->
<div class="input-demo">
<input v-model="message" placeholder="输入一些内容..." />
<p>输入的内容: {{ message }}</p>
</div>
<!-- 时间显示 -->
<div class="time">
<p>组件创建时间: {{ createdTime }}</p>
<p>最后激活时间: {{ activatedTime }}</p>
<p>激活次数: {{ activatedCount }}</p>
</div>
</div>
</template>
<script setup lang="ts">
import { ref, onMounted, onUnmounted, onActivated, onDeactivated } from 'vue'
// 组件属性
const props = defineProps<{
title?: string
}>()
// 响应式数据
const count = ref(0)
const message = ref('')
const createdTime = ref('')
const activatedTime = ref('')
const activatedCount = ref(0)
// 生命周期钩子
onMounted(() => {
createdTime.value = new Date().toLocaleTimeString()
console.log(`组件 ${props.title || '未知'} mounted`)
})
onUnmounted(() => {
console.log(`组件 ${props.title || '未知'} unmounted`)
})
// KeepAlive 特有钩子
onActivated(() => {
activatedTime.value = new Date().toLocaleTimeString()
activatedCount.value++
console.log(`组件 ${props.title || '未知'} activated`)
})
onDeactivated(() => {
console.log(`组件 ${props.title || '未知'} deactivated`)
})
</script>
<style scoped>
.tab-content {
padding: 20px;
border: 1px solid #e8e8e8;
border-radius: 4px;
background: white;
}
.counter, .input-demo, .time {
margin: 15px 0;
padding: 10px;
border: 1px dashed #ddd;
}
input {
padding: 8px;
width: 300px;
margin-right: 10px;
}
</style>
路由级别的 KeepAlive:
// src/router/index.ts
import { createRouter, createWebHistory } from 'vue-router'
import Home from '@/views/Home.vue'
import About from '@/views/About.vue'
import KeepAliveDemo from '@/views/KeepAliveDemo.vue'
const routes = [
{
path: '/',
name: 'Home',
component: Home,
meta: {
keepAlive: true // 标记需要缓存
}
},
{
path: '/about',
name: 'About',
component: About,
meta: {
keepAlive: false // 不缓存
}
},
{
path: '/keepalive',
name: 'KeepAliveDemo',
component: KeepAliveDemo
}
]
const router = createRouter({
history: createWebHistory(),
routes,
// 滚动行为:保持滚动位置
scrollBehavior(to, from, savedPosition) {
if (savedPosition) {
return savedPosition
} else {
return { top: 0 }
}
}
})
export default router
<!-- src/App.vue -->
<template>
<div id="app">
<nav>
<router-link to="/">首页</router-link>
<router-link to="/about">关于</router-link>
<router-link to="/keepalive">KeepAlive 演示</router-link>
</nav>
<!-- 路由视图使用 KeepAlive -->
<router-view v-slot="{ Component, route }">
<KeepAlive :include="cachedRoutes">
<component
:is="Component"
:key="route.fullPath"
/>
</KeepAlive>
</router-view>
</div>
</template>
<script setup lang="ts">
import { computed } from 'vue'
import { useRouter } from 'vue-router'
const router = useRouter()
// 计算需要缓存的路由
const cachedRoutes = computed(() => {
return router.getRoutes()
.filter(route => route.meta?.keepAlive)
.map(route => route.name as string)
.filter(Boolean)
})
</script>
5. 高级功能演示
include/exclude 配置:
<template>
<div>
<h3>include/exclude 演示</h3>
<!-- 只缓存 TabA 和 TabB -->
<KeepAlive :include="['TabA', 'TabB']">
<component :is="currentComponent" />
</KeepAlive>
<!-- 排除 TabC -->
<KeepAlive :exclude="['TabC']">
<component :is="currentComponent" />
</KeepAlive>
<!-- 使用正则表达式 -->
<KeepAlive :include="/^Tab/">
<component :is="currentComponent" />
</KeepAlive>
</div>
</template>
max 属性限制缓存数量:
<template>
<div>
<h3>max 属性演示(LRU 缓存)</h3>
<!-- 最多缓存 3 个组件 -->
<KeepAlive :max="3">
<component :is="currentComponent" :key="currentKey" />
</KeepAlive>
<div class="controls">
<button
v-for="i in 5"
:key="i"
@click="switchToComponent(`Component${i}`)"
切换到组件 {{ i }}
</button>
</div>
<div class="cache-status">
<p>当前组件: {{ currentKey }}</p>
<p>缓存队列: {{ cacheQueue.join(' → ') }}</p>
</div>
</div>
</template>
<script setup lang="ts">
import { ref } from 'vue'
const currentKey = ref('Component1')
const cacheQueue = ref<string[]>(['Component1'])
function switchToComponent(key: string) {
currentKey.value = key
// 模拟 LRU 队列更新
const index = cacheQueue.value.indexOf(key)
if (index > -1) {
// 已存在,移动到末尾
cacheQueue.value.splice(index, 1)
}
cacheQueue.value.push(key)
// 保持最多 3 个
if (cacheQueue.value.length > 3) {
cacheQueue.value.shift()
}
}
</script>
滚动位置保持:
<template>
<div class="scroll-demo">
<h3>滚动位置保持演示</h3>
<KeepAlive>
<ScrollContent :key="currentTab" />
</KeepAlive>
<div class="tabs">
<button @click="currentTab = 'list1'">列表 1</button>
<button @click="currentTab = 'list2'">列表 2</button>
<button @click="currentTab = 'list3'">列表 3</button>
</div>
</div>
</template>
<script setup lang="ts">
import { ref } from 'vue'
import ScrollContent from './ScrollContent.vue'
const currentTab = ref('list1')
</script>
<style scoped>
.scroll-demo {
height: 500px;
display: flex;
flex-direction: column;
}
.tabs {
margin-top: 20px;
}
</style>
<!-- ScrollContent.vue -->
<template>
<div class="scroll-content" ref="scrollContainer">
<div v-for="item in 100" :key="item" class="item">
项目 {{ item }}
</div>
</div>
</template>
<script setup lang="ts">
import { ref, onActivated, onDeactivated } from 'vue'
const scrollContainer = ref<HTMLElement>()
let scrollTop = 0
// 保存滚动位置
onDeactivated(() => {
if (scrollContainer.value) {
scrollTop = scrollContainer.value.scrollTop
}
})
// 恢复滚动位置
onActivated(() => {
if (scrollContainer.value) {
scrollContainer.value.scrollTop = scrollTop
}
})
</script>
<style scoped>
.scroll-content {
height: 400px;
overflow-y: auto;
border: 1px solid #ddd;
padding: 10px;
}
.item {
padding: 15px;
border-bottom: 1px solid #eee;
}
.item:hover {
background: #f5f5f5;
}
</style>
六、源码深度解析
1. 缓存数据结构
// Vue 3 KeepAlive 的缓存实现
interface KeepAliveCache {
// 缓存映射
cache: Map<string | number | symbol, VNode>
// 键的访问顺序(LRU)
keys: Set<string | number | symbol>
// 添加缓存
set(key: string | number | symbol, vnode: VNode): void
// 获取缓存
get(key: string | number | symbol): VNode | undefined
// 删除缓存
delete(key: string | number | symbol): boolean
// 清空缓存
clear(): void
}
2. 组件激活/失活流程
// 组件激活流程
function activateComponent(vnode: VNode) {
// 1. 恢复组件实例
const instance = vnode.component!
// 2. 调用 activated 钩子
if (instance.activated) {
callHook(instance, 'activated')
}
// 3. 恢复子组件
if (vnode.children) {
for (const child of vnode.children) {
if (isVNode(child) && child.component) {
activateComponent(child)
}
}
}
}
// 组件失活流程
function deactivateComponent(vnode: VNode) {
// 1. 调用 deactivated 钩子
const instance = vnode.component!
if (instance.deactivated) {
callHook(instance, 'deactivated')
}
// 2. 失活子组件
if (vnode.children) {
for (const child of vnode.children) {
if (isVNode(child) && child.component) {
deactivateComponent(child)
}
}
}
}
3. include/exclude 匹配算法
function matches(pattern: string | RegExp | Array<string | RegExp>, name: string): boolean {
if (Array.isArray(pattern)) {
return pattern.some(p => matches(p, name))
} else if (typeof pattern === 'string') {
return pattern.split(',').map(s => s.trim()).includes(name)
} else if (pattern instanceof RegExp) {
return pattern.test(name)
}
return false
}
七、性能优化与最佳实践
1. 合理使用 include/exclude
<!-- 只缓存需要保持状态的组件 -->
<KeepAlive :include="['UserList', 'ProductList', 'OrderList']">
<router-view />
</KeepAlive>
<!-- 排除不需要缓存的组件 -->
<KeepAlive :exclude="['Login', 'Register', 'ErrorPage']">
<router-view />
</KeepAlive>
2. 设置合理的 max 值
<!-- 根据应用场景设置缓存上限 -->
<KeepAlive :max="5"> <!-- 标签页应用 -->
<KeepAlive :max="10"> <!-- 后台管理系统 -->
<KeepAlive :max="3"> <!-- 移动端应用 -->
3. 避免内存泄漏
// 在组件卸载时清理资源
onDeactivated(() => {
// 清理定时器
if (this.timer) {
clearInterval(this.timer)
this.timer = null
}
// 清理事件监听
window.removeEventListener('resize', this.handleResize)
// 清理 WebSocket 连接
if (this.ws) {
this.ws.close()
}
})
onActivated(() => {
// 重新初始化资源
this.timer = setInterval(() => {
this.updateData()
}, 5000)
window.addEventListener('resize', this.handleResize)
this.connectWebSocket()
})
4. 结合路由使用
// 路由配置
const routes = [
{
path: '/list',
component: ListPage,
meta: {
keepAlive: true,
scrollTop: 0 // 记录滚动位置
}
},
{
path: '/detail/:id',
component: DetailPage,
meta: {
keepAlive: false
}
}
]
// 路由守卫中处理缓存
router.beforeEach((to, from, next) => {
// 从详情页返回列表页时,保持列表页缓存
if (from.name === 'DetailPage' && to.name === 'ListPage') {
// 保持缓存
} else {
// 其他情况可以清除缓存
}
next()
})
八、常见问题与解决方案
1. 组件不缓存的问题
<!-- 问题:组件没有 key,无法缓存 -->
<KeepAlive>
<Component /> <!-- 缺少 key -->
</KeepAlive>
<!-- 解决方案:添加唯一的 key -->
<KeepAlive>
<Component :key="componentKey" />
</KeepAlive>
<!-- 或者使用路由的 fullPath -->
<router-view v-slot="{ Component }">
<KeepAlive>
<component :is="Component" :key="$route.fullPath" />
</KeepAlive>
</router-view>
2. 数据不更新的问题
<script setup>
// 问题:缓存组件的数据不会自动更新
const { data } = await fetchData() // 只在 mounted 时执行
// 解决方案:在 activated 钩子中更新数据
onActivated(async () => {
const { data } = await fetchData()
// 更新数据
})
</script>
3. 滚动位置问题
// 问题:KeepAlive 不自动保存滚动位置
// 解决方案:手动保存和恢复
const scrollTop = ref(0)
onDeactivated(() => {
scrollTop.value = document.documentElement.scrollTop
})
onActivated(() => {
window.scrollTo(0, scrollTop.value)
})
4. 内存泄漏问题
// 问题:缓存组件中的定时器、事件监听器等未清理
// 解决方案:在 deactivated 中清理
let timer: number | null = null
onMounted(() => {
timer = setInterval(() => {
console.log('定时器执行')
}, 1000)
})
onDeactivated(() => {
if (timer) {
clearInterval(timer)
timer = null
}
})
onActivated(() => {
if (!timer) {
timer = setInterval(() => {
console.log('定时器重新启动')
}, 1000)
}
})
九、与 React 的对比
| 特性 | Vue KeepAlive | React 类似方案 | 区别 |
|---|---|---|---|
| 内置支持 | 是 | 否(需要第三方库) | Vue 原生支持 |
| 生命周期 | activated/deactivated | 无对应钩子 | Vue 更完善 |
| 缓存策略 | LRU 算法 | 手动管理 | Vue 自动管理 |
| 配置方式 | include/exclude/max | 手动实现 | Vue 更简单 |
| 路由集成 | 容易 | 需要配置 | Vue 更友好 |
React 实现类似功能:
// 使用 react-activation
import { AliveScope, KeepAlive } from 'react-activation'
function App() {
return (
<AliveScope>
<KeepAlive id="unique-id" cacheKey="cache-key">
<Component />
</KeepAlive>
</AliveScope>
)
}
十、总结
Vue KeepAlive 是一个强大的组件缓存解决方案:
- 核心价值:提升性能,改善用户体验
- 实现原理:基于 LRU 算法的智能缓存管理
- 生命周期:完整的 activated/deactivated 钩子
- 灵活配置:include/exclude/max 满足不同需求
最佳实践:
- 为需要缓存的组件添加唯一的 key
- 合理���置 include/exclude 避免不必要的缓存
- 根据应用场景设置合适的 max 值
- 在 activated/deactivated 中管理资源
- 结合路由实现完整的页面缓存方案
最后提醒:KeepAlive 不是银弹,过度使用会导致内存占用过高。需要根据实际场景权衡缓存带来的性能提升和内存消耗。
如果对你有用,欢迎点赞、收藏、关注! 下一篇我们将介绍 10 个能大幅提升开发效率的 npm 工具包。
参考资料: