VUE中的keep-alive详解和应用详细教程
一、概述
在 Vue 应用中,组件的创建与销毁会带来一定的性能开销,并且在切换视图时,组件的状态(如表单数据、滚动位置等)很容易丢失。Vue 官方提供了 <keep-alive>
组件,用于缓存动态组件的实例,从而避免重复渲染与数据丢失。本文将从原理、常用属性、生命周期钩子、典型应用场景等方面,结合代码示例与图解,帮助你深入理解并灵活应用 keep-alive
。
二、keep-alive
原理
<keep-alive>
是 Vue 内置的抽象组件(abstract component),它不会渲染为实际的 DOM 节点,而是包裹其下的子组件实例,控制它们是销毁还是缓存。默认情况下,当一个被 <keep-alive>
包裹的组件从视图移除时,不会立即销毁组件实例,而是将其实例保留在内存中;当再次切换回来时,会继续复用之前的实例,保留先前的状态。
2.1 缓存流程示意图
以下使用简化的 ASCII 图示,帮助理解组件挂载与缓存的过程:
┌──────────────┐ 切换视图 ┌──────────────┐
│ 组件 A 挂载 │ -----------------> │ 组件 A 缓存 │
│ (mounted) │ <----------------- │ (cached,无销毁)│
│ │ 再次显示 │ │
└──────────────┘ └──────────────┘
┌──────────────┐ 切换视图 ┌──────────────┐
│ 组件 B 挂载 │ -----------------> │ 组件 B 缓存 │
│ (mounted) │ <----------------- │ (cached,无销毁)│
│ │ 再次显示 │ │
└──────────────┘ └──────────────┘
当未使用 <keep-alive>
时,组件 A 和组件 B 互相切换就会被销毁再重新挂载(destroyed
→ created
→ mounted
)。而包裹在 <keep-alive>
下的组件,则是第一次挂载后进入“缓存”,再次切换回来时直接复用,不重复走创建与销毁流程。
三、基础用法与示例
3.1 动态组件方式
最常见的场景是结合 <component :is="…">
动态组件使用 <keep-alive>
。例如:在页面中,通过按钮或 Tabs 切换不同组件:
<template>
<div>
<!-- 切换按钮 -->
<button @click="active = 'CompA'">显示 A</button>
<button @click="active = 'CompB'">显示 B</button>
<!-- 将动态组件包裹在 keep-alive -->
<keep-alive>
<component :is="active" />
</keep-alive>
</div>
</template>
<script>
import CompA from './CompA.vue'
import CompB from './CompB.vue'
export default {
components: { CompA, CompB },
data() {
return {
active: 'CompA' // 初始显示 CompA
}
}
}
</script>
- 启动时:
active='CompA'
,Vue 创建并挂载CompA
,同时保留其实例。 - 切换到
CompB
时:CompA
被移动出视图,但因为被keep-alive
包裹,实际并不销毁,而是进入缓存;随后创建并挂载CompB
,同理也缓存。 - 再次切换到
CompA
时:直接复用缓存的CompA
实例,不重新执行created
、mounted
。
3.2 路由组件方式
在 Vue-Router 场景下,经常把 <router-view>
放入 <keep-alive>
,为指定的路由组件实现缓存:
<template>
<div>
<!-- 仅缓存以下两个路由的组件 -->
<keep-alive include="Home,About">
<router-view />
</keep-alive>
</div>
</template>
- 当路由切换到
/home
时挂载Home.vue
;切换到/about
时挂载About.vue
。两者都会被缓存下来。 - 切换到
/contact
时,Contact.vue
不在 include 列表内,会正常销毁,不会缓存。
注意:要使路由组件被keep-alive
缓存,需要在组件上设置name
,如export default { name: 'Home', … }
。
四、常用属性
<keep-alive>
提供了几个可选属性,用于精细控制哪些组件需要缓存、缓存上限等。
4.1 include
与 exclude
include
:一个字符串或正则表达式,只有名称匹配include
的组件才会被缓存。exclude
:与include
相反,名称匹配exclude
的组件不会被缓存。
优先级:如果同时使用include
和exclude
,exclude
优先级更高,即先判断是否在排除列表内,如果命中则不缓存。
<keep-alive include="CompA,CompB" exclude="CompC">
<component :is="active" />
</keep-alive>
- 只有
CompA
和CompB
会缓存; CompC
则无论是否出现在include
中,都会被排除,不缓存。
可以使用正则表达式来匹配多个组件名称:
<!-- 缓存所有名称以 Page 开头的组件 -->
<keep-alive include="/^Page.*/">
<component :is="active" />
</keep-alive>
4.2 max
max
:指定缓存大小上限(组件实例的最大数量)。当缓存数量超出max
时,最早被缓存(最久未访问)的组件会被销毁并淘汰。
<keep-alive max="2">
<component :is="active" />
</keep-alive>
- 假设先后切换组件为
A → B → C
,此时缓存中有A, B, C
三个实例,超过了max=2
,那么最先缓存的A
会被销毁,缓存只保留B, C
。
五、生命周期钩子
当组件被 keep-alive
缓存时,会触发两个额外的生命周期钩子:activated
和 deactivated
。它们用于替代普通组件的 mounted
、destroyed
来进行针对“缓存/复用”场景下的初始化/清理操作。
activated
:当组件被激活(从缓存中恢复)时调用。deactivated
:当组件被停用(移动到缓存中,但未销毁)时调用。
<!-- Example.vue -->
<template>
<div>
<p>示例组件。当前计数:{{ count }}</p>
<button @click="count++">递增</button>
</div>
</template>
<script>
export default {
name: 'Example',
data() {
return {
count: 0
}
},
mounted() {
console.log('mounted:组件初次挂载')
},
activated() {
console.log('activated:组件从缓存中恢复')
},
deactivated() {
console.log('deactivated:组件被缓存')
},
beforeDestroy() {
console.log('beforeDestroy:组件销毁前')
},
destroyed() {
console.log('destroyed:组件真正销毁')
}
}
</script>
- 第一次
active='Example'
时,先执行created -> mounted
。 - 切换到其他组件,此时触发
deactivated
(而非beforeDestroy
、destroyed
)。 - 再次切换回
Example
,执行activated
。 - 如果因
max
或exclude
等原因被真正销毁,则beforeDestroy → destroyed
会被调用。
六、详细图解:keep-alive
缓存流程
下面用一张更详细的 ASCII 流程图演示 keep-alive
对组件的挂载、缓存、激活与销毁过程。
┌─────────────────────────────────────────────────────────────────┐
│ 初次渲染(active = A) │
│ │
│ <keep-alive> │
│ <component is="A" /> → Vue 真正执行 new A() 实例化,并挂载 │
│ </keep-alive> │
│ │
│ 过程: │
│ 1. A.created │
│ 2. A.mounted │
│ 3. 缓存池:{ A 实例 } │
└─────────────────────────────────────────────────────────────────┘
切换到 B (active = B)
┌─────────────────────────────────────────────────────────────────┐
│ 缓存 A,并挂载 B │
│ │
│ <keep-alive> │
│ A 实例 (状态:cached, 调用 A.deactivated) │
│ <component is="B" /> → new B() │
│ </keep-alive> │
│ │
│ 过程: │
│ 1. A.deactivated │
│ 2. B.created │
│ 3. B.mounted │
│ 4. 缓存池:{ A 实例, B 实例 } │
└─────────────────────────────────────────────────────────────────┘
再次切换到 A (active = A)
┌─────────────────────────────────────────────────────────────────┐
│ 从缓存中恢复 A,并停用 B │
│ │
│ <keep-alive> │
│ <component is="A" /> → 复用 A 实例,调用 A.activated │
│ B 实例 (调用 B.deactivated) │
│ </keep-alive> │
│ │
│ 过程: │
│ 1. B.deactivated │
│ 2. A.activated │
│ 3. 缓存池:{ A 实例, B 实例 } │
└─────────────────────────────────────────────────────────────────┘
缓存数超出 max=1
┌─────────────────────────────────────────────────────────────────┐
│ 假设 max=1,缓存池已有 A, B,此时挂载 C (active = C) │
│ │
│ <keep-alive max="1"> │
│ A 实例 → 淘汰最早缓存 A(调用 A.beforeDestroy、A.destroyed) │
│ B 实例 → 调用 B.deactivated │
│ <component is="C" /> → new C() │
│ </keep-alive> │
│ │
│ 过程: │
│ 1. A.beforeDestroy │
│ 2. A.destroyed │
│ 3. B.deactivated │
│ 4. C.created │
│ 5. C.mounted │
│ 6. 缓存池:{ B 实例, C 实例 } (A 被彻底销毁) │
└─────────────────────────────────────────────────────────────────┘
要点:
- 首次挂载:组件走
created
→mounted
,并进入缓存池。- 切换非当前组件:停用组件,走
deactivated
,保留在缓存池。- 再次回到缓存组件:直接复用,不重复创建,走
activated
。- 缓存数超限:最久未访问(最先缓存)的组件会被淘汰,走
beforeDestroy
→destroyed
。
七、典型应用场景
7.1 页面/标签切换时保持状态
在多标签、Tab 或侧边栏导航的场景中,希望返回到先前的页面时,仍能保持之前的滚动位置、表单填写状态或页面数据。示例如下:
<template>
<div>
<!-- 顶部 Tabs -->
<el-tabs v-model="activeTab">
<el-tab-pane label="用户列表" name="UserList" />
<el-tab-pane label="设置中心" name="Settings" />
</el-tabs>
<!-- 用 keep-alive 缓存选项卡组件 -->
<keep-alive>
<component :is="activeTab" />
</keep-alive>
</div>
</template>
<script>
import UserList from './UserList.vue'
import Settings from './Settings.vue'
export default {
components: { UserList, Settings },
data() {
return {
activeTab: 'UserList'
}
}
}
</script>
- 效果:在
UserList
与Settings
之间切换时,UserList
组件的数据与滚动位置会保持;无需重新请求接口或重新渲染表格数据。
7.2 路由页面缓存(路由复用)
对于较大型的 SPA 应用,某些页面切换成本较高(如需要重新请求接口、重新初始化图表等),借助 <keep-alive>
可以在返回时快速响应。以 Vue-Router 为例:
// router/index.js
import { createRouter, createWebHistory } from 'vue-router'
import Home from '@/views/Home.vue'
import Dashboard from '@/views/Dashboard.vue'
import Profile from '@/views/Profile.vue'
const routes = [
{ path: '/', component: Home, name: 'Home' },
{ path: '/dashboard', component: Dashboard, name: 'Dashboard' },
{ path: '/profile', component: Profile, name: 'Profile' }
]
const router = createRouter({
history: createWebHistory(),
routes
})
export default router
<!-- App.vue -->
<template>
<div id="app">
<router-link to="/">Home</router-link>
<router-link to="/dashboard">Dashboard</router-link>
<router-link to="/profile">Profile</router-link>
<!-- 仅缓存 Dashboard 和 Profile 页面 -->
<keep-alive include="Dashboard,Profile">
<router-view />
</keep-alive>
</div>
</template>
- 在从
Dashboard
跳转到Home
,再返回时,Dashboard
依然保持之前的状态(如图表滚动、过滤条件等); Home
并未被缓存,每次进入都会重新渲染。
7.3 表单/富文本编辑状态保持
在多步骤表单或富文本编辑场景下,用户填写到一半切换到其他页面,再返回时希望输入内容不丢失:
<!-- FormStep1.vue -->
<template>
<div>
<h3>第一步:填写个人信息</h3>
<el-form :model="form">
<el-form-item label="姓名">
<el-input v-model="form.name" />
</el-form-item>
<!-- 更多表单项 -->
</el-form>
</div>
</template>
<script>
export default {
name: 'FormStep1',
data() {
return {
form: {
name: '',
age: null
}
}
},
activated() {
console.log('表单组件被恢复,保留之前输入')
},
deactivated() {
console.log('表单组件被切换,缓存数据')
}
}
</script>
<!-- FormStepContainer.vue -->
<template>
<div>
<el-steps :active="currentStep">
<el-step title="个人信息" />
<el-step title="联系方式" />
<el-step title="提交" />
</el-steps>
<keep-alive>
<component :is="currentComponent" />
</keep-alive>
<div>
<el-button @click="prev" :disabled="currentStep === 0">上一步</el-button>
<el-button @click="next" :disabled="currentStep === steps.length - 1">下一步</el-button>
</div>
</div>
</template>
<script>
import FormStep1 from './FormStep1.vue'
import FormStep2 from './FormStep2.vue'
import FormStep3 from './FormStep3.vue'
export default {
components: { FormStep1, FormStep2, FormStep3 },
data() {
return {
steps: ['FormStep1', 'FormStep2', 'FormStep3'],
currentStep: 0
}
},
computed: {
currentComponent() {
return this.steps[this.currentStep]
}
},
methods: {
next() {
if (this.currentStep < this.steps.length - 1) {
this.currentStep++
}
},
prev() {
if (this.currentStep > 0) {
this.currentStep--
}
}
}
}
</script>
- 使用
<keep-alive>
包裹三个步骤的组件,可以保证在切换时,表单数据和用户输入不丢失。 - 在组件的
activated
和deactivated
钩子中,可以根据需要做二次校验、重置焦点等操作。
7.4 性能优化:避免重复渲染
在一些数据量较大的页面(如后台列表、大规模图表、三维可视化)中,重复销毁与创建组件特别浪费资源。可以借助 keep-alive
缓存视图,提升切换速度与用户体验。
<template>
<div class="dashboard-container">
<el-menu v-model="activeMenu" @select="onSelect">
<el-menu-item index="analytics">Analytics</el-menu-item>
<el-menu-item index="reports">Reports</el-menu-item>
</el-menu>
<keep-alive>
<component :is="currentView" />
</keep-alive>
</div>
</template>
<script>
import Analytics from './Analytics.vue'
import Reports from './Reports.vue'
export default {
components: { Analytics, Reports },
data() {
return {
activeMenu: 'analytics'
}
},
computed: {
currentView() {
return this.activeMenu === 'analytics' ? 'Analytics' : 'Reports'
}
},
methods: {
onSelect(menu) {
this.activeMenu = menu
}
}
}
</script>
- 节省网络与数据请求:
Analytics.vue
可能会花费几秒钟加载大型图表,缓存后再次切换回来会立即显示。 - 避免销毁重建 DOM:大规模 DOM 重建会引起卡顿,
keep-alive
可使组件保留在内存中,保持绘图状态。
八、进阶应用
8.1 动态控制缓存
在某些场景下,需要根据业务逻辑动态决定是否缓存。例如:当用户选中了“开启缓存”复选框时,才缓存组件;否则每次都销毁重建。
<template>
<div>
<el-checkbox v-model="useCache">开启视图缓存</el-checkbox>
<component :is="useCache ? 'keep-alive' : 'div'">
<component :is="currentTab" />
</component>
</div>
</template>
<script>
import TabA from './TabA.vue'
import TabB from './TabB.vue'
export default {
components: { TabA, TabB },
data() {
return {
currentTab: 'TabA',
useCache: true
}
}
}
</script>
- 当
useCache = true
时,<component is="keep-alive">
等价于<keep-alive>
,会缓存TabA
/TabB
。 - 当
useCache = false
时,<component is="div">
仅是一个div
容器,不会触发缓存逻辑,每次切换都销毁重建。
8.2 手动清除缓存
Vue2 中可以通过 <keep-alive :include>
或动态修改 key
来控制缓存。Vue3 提供了 <KeepAlive>
对应的 JavaScript API,例如 $refs.keepAliveComponent && $refs.keepAliveComponent.cache
等(内部缓存对象),但官方并不推荐通过私有 API 操作。
更安全的方式是,通过改变组件名或修改 include
列表,让原缓存失效。例如:将组件 name
改为动态值,迫使 keep-alive
认为是新组件,重新挂载并丢弃旧缓存。
<!-- 通过唯一标识作为 name,让 keep-alive 每次都认为是新组件 -->
<keep-alive>
<component :is="currentComponent" :key="cacheKey" />
</keep-alive>
<!-- 当需要清空缓存时,只需修改 cacheKey -->
<script>
export default {
data() {
return {
currentComponent: 'MyComp',
cacheKey: 'v1'
}
},
methods: {
clearCache() {
this.cacheKey = 'v' + Date.now() // 随机更新 key,清空旧缓存
}
}
}
</script>
九、完整示例:综合应用
下面展示一个稍微完整些的示例——基于 Vue-Router 的多页面应用,演示如何使用 keep-alive
缓存部分路由并结合生命周期钩子做额外处理。
9.1 项目目录结构
src/
├─ App.vue
├─ main.js
└─ views/
├─ Home.vue
├─ Dashboard.vue
└─ Profile.vue
router/
└─ index.js
9.2 路由配置(router/index.js
)
import { createRouter, createWebHistory } from 'vue-router'
import Home from '@/views/Home.vue'
import Dashboard from '@/views/Dashboard.vue'
import Profile from '@/views/Profile.vue'
const routes = [
{ path: '/', name: 'Home', component: Home },
{ path: '/dashboard', name: 'Dashboard', component: Dashboard },
{ path: '/profile', name: 'Profile', component: Profile }
]
const router = createRouter({
history: createWebHistory(),
routes
})
export default router
9.3 根组件(App.vue
)
<template>
<div id="app">
<nav>
<router-link to="/">Home</router-link> |
<router-link to="/dashboard">Dashboard</router-link> |
<router-link to="/profile">Profile</router-link>
</nav>
<!-- 只缓存 Dashboard 和 Profile -->
<keep-alive include="Dashboard,Profile">
<router-view />
</keep-alive>
</div>
</template>
<script>
export default {
name: 'App'
}
</script>
include="Dashboard,Profile"
:只有名称为Dashboard
或Profile
的组件会被缓存。Home
每次切换到该路由都会重新渲染。
9.4 页面组件示例
9.4.1 Dashboard.vue
<template>
<div>
<h2>Dashboard</h2>
<p>这是一个会被缓存的页面。</p>
<p>当前时间:{{ now }}</p>
<button @click="showCount++">增加计数 ({{ showCount }})</button>
</div>
</template>
<script>
export default {
name: 'Dashboard',
data() {
return {
now: new Date().toLocaleTimeString(),
showCount: 0,
timer: null
}
},
mounted() {
this.startTimer()
console.log('Dashboard mounted')
},
activated() {
this.startTimer()
console.log('Dashboard activated')
},
deactivated() {
clearInterval(this.timer)
console.log('Dashboard deactivated')
},
beforeDestroy() {
console.log('Dashboard beforeDestroy')
},
destroyed() {
console.log('Dashboard destroyed')
},
methods: {
startTimer() {
this.timer = setInterval(() => {
this.now = new Date().toLocaleTimeString()
}, 1000)
}
}
}
</script>
- 在 首次挂载 或 从缓存恢复 时,
mounted
与activated
都会被调用,此处都启动定时器更新时间。 - 当切换到其他路由时,走
deactivated
,清除定时器,避免内存泄漏。 - 如果组件因某些原因被销毁(如缓存淘汰),会触发
beforeDestroy
→destroyed
。
9.4.2 Profile.vue
<template>
<div>
<h2>Profile</h2>
<el-form :model="form">
<el-form-item label="用户名">
<el-input v-model="form.username" />
</el-form-item>
<el-form-item label="邮箱">
<el-input v-model="form.email" />
</el-form-item>
</el-form>
</div>
</template>
<script>
export default {
name: 'Profile',
data() {
return {
form: {
username: '',
email: ''
}
}
},
activated() {
console.log('Profile 组件被激活,保留之前的表单数据')
},
deactivated() {
console.log('Profile 组件被停用,表单数据仍保留在缓存中')
}
}
</script>
- 即使从
Profile
跳转到别的页面,返回时能看到先前填写的表单内容。
9.4.3 Home.vue
<template>
<div>
<h2>Home</h2>
<p>这是一个不被缓存的页面,每次都会重新加载。</p>
<button @click="count += 1">计数 ({{ count }})</button>
</div>
</template>
<script>
export default {
name: 'Home',
data() {
return {
count: 0
}
},
mounted() {
console.log('Home mounted,count 重置为 0')
}
}
</script>
Home
因未在include
中,被每次切换时都会重新创建与销毁,count
始终从0
开始。
十、小结与注意事项
- 缓存的本质:
<keep-alive>
并不会生成额外的 DOM 节点,而是将组件实例缓存在内存中。 - 组件
name
必须唯一:要让include
/exclude
生效,需要给每个组件设置唯一的name
属性。 - 合理设置
max
:根据应用场景与内存开销,控制缓存大小,防止内存占用过高。 - 正确清理定时器与订阅:在
deactivated
钩子中清理setInterval
、setTimeout
、WebSocket、订阅等资源,避免内存泄漏。 - 注意 props 或 dynamic key 的变化:当组件的
key
发生改变时,会被视为全新组件,先前缓存会失效并销毁。可用作手动清缓存手段。 - 服务端渲染(SSR)下不支持缓存:Vue SSR 无法在服务端缓存组件实例,
keep-alive
仅在客户端有效。
通过本文,你应当对 keep-alive
的原理、属性、生命周期钩子以及典型应用场景有了系统的理解。熟练运用 keep-alive
,能让你的 Vue 应用在性能与用户体验上更加出色。祝你学习顺利!
评论已关闭