哈曼卡顿Harman Kardon One(哈曼卡顿智能音箱)
156.1MB · 2026-03-12
在现代 Web 应用中,页面切换的流畅度和状态保持是用户体验的关键因素。我们在开发中应该也遇到过这样的场景:填写了一半的表单,切换到其他页面再回来,输入的内容全部清空/丢失;或者频繁切换的标签页每次都要重新渲染,导致性能下降。
因此,Vue 提供了两个强大的特性来解决这些问题:动态组件 和 keep-alive。动态组件让我们可以灵活地切换不同组件,而 keep-alive 则能缓存组件状态,避免重复渲染。本文将深入探讨它们的原理、用法和最佳实践。
<component :is> 内置组件Vue 提供了 <component> 内置组件,通过 :is 属性动态渲染不同的组件:
<template>
<div class="tab-container">
<div class="tab-header">
<button
v-for="tab in tabs"
:key="tab.name"
@click="currentTab = tab.component"
:class="{ active: currentTab === tab.component }"
>
{{ tab.title }}
</button>
</div>
<div class="tab-content">
<!-- 动态渲染当前选中的组件 -->
<component :is="currentTab" />
</div>
</div>
</template>
<script setup>
import { ref } from 'vue'
import Profile from './Profile.vue'
import Settings from './Settings.vue'
import Notifications from './Notifications.vue'
const tabs = [
{ title: '个人资料', component: Profile },
{ title: '系统设置', component: Settings },
{ title: '通知管理', component: Notifications }
]
const currentTab = ref(Profile)
</script>
<component :is> 工作原理:is 可以接收组件选项对象、注册的组件名或导入的组件unmounted 和 mounted 生命周期当组件较大或不需要立即加载时,可以结合 defineAsyncComponent 实现按需加载:
<template>
<div>
<button @click="loadDashboard">加载仪表盘</button>
<component :is="dashboardComp" />
</div>
</template>
<script setup>
import { ref, defineAsyncComponent } from 'vue'
const dashboardComp = ref(null)
// 点击按钮时才加载组件
async function loadDashboard() {
dashboardComp.value = defineAsyncComponent(() =>
import('./Dashboard.vue')
)
}
</script>
更常见的用法是在路由中配置异步组件:
// router/index.ts
import { createRouter, createWebHistory } from 'vue-router'
const router = createRouter({
history: createWebHistory(),
routes: [
{
path: '/dashboard',
component: () => import('./views/Dashboard.vue') // 路由级别异步加载
},
{
path: '/reports',
component: () => import('./views/Reports.vue')
}
]
})
| 实现方式 | 适用场景 | 优点 | 缺点 |
|---|---|---|---|
<component :is> | 固定组件集合内的切换 | 简单直观,切换快速 | 所有组件都会被打包 |
| 动态导入 + 异步 | 大型组件、路由级别 | 减少首屏体积 | 切换时有加载延迟 |
默认情况下,动态组件切换时会销毁旧组件,但组件在用 <keep-alive> 包裹后,会被缓存,组件切换时不会被销毁:
<template>
<keep-alive>
<component :is="currentTab" />
</keep-alive>
</template>
graph TD
A[组件切换] --> B{是否在缓存中?}
B -->|是| C[从缓存恢复实例和 DOM]
B -->|否| D[创建新实例]
D --> E[加入缓存]
C --> F[触发 activated]
E --> F
keep-alive 内部使用 LRU(Least Recently Used) 算法管理缓存:
// 简化的 LRU 实现
class KeepAliveCache {
private cache = new Map()
private keys = new Set()
constructor(private max: number) {}
get(key) {
if (this.cache.has(key)) {
// 刷新 key 的位置(最近使用)
this.keys.delete(key)
this.keys.add(key)
return this.cache.get(key)
}
return null
}
set(key, value) {
if (this.cache.size >= this.max) {
// 删除最久未使用的(第一个 key)
const oldestKey = this.keys.values().next().value
this.cache.delete(oldestKey)
this.keys.delete(oldestKey)
}
this.cache.set(key, value)
this.keys.add(key)
}
}
通过 include 和 exclude 属性,我们可以精细控制哪些组件被缓存:
<template>
<!-- 只缓存名称匹配的组件 -->
<keep-alive include="Profile,Settings">
<component :is="currentTab" />
</keep-alive>
<!-- 排除某些组件 -->
<keep-alive exclude="Notifications">
<component :is="currentTab" />
</keep-alive>
<!-- 支持正则和数组 -->
<keep-alive :include="/tab|view/">
<router-view />
</keep-alive>
<keep-alive :include="['Profile', 'Settings']">
<router-view />
</keep-alive>
</template>
特别注意:include/exclude 匹配的是组件的 name 选项:
// 组件必须有 name 才能被 include/exclude 匹配
export default {
name: 'Profile',
// ...
}
// 或者使用 <script setup> 时
<script setup>
defineOptions({
name: 'Profile'
})
</script>
当组件被 keep-alive 缓存时,会多出两个生命周期钩子:onActivated 和 onDeactivated ,分别对应 组件激活时 和 组件失活时 的生命周期:
<script setup>
import { onActivated, onDeactivated, onMounted, onUnmounted } from 'vue'
onMounted(() => {
console.log('组件首次挂载')
})
onUnmounted(() => {
console.log('组件卸载(从缓存中移除)')
})
onActivated(() => {
console.log('组件激活(从缓存恢复到页面)')
// 适合恢复轮询、恢复动画、重新计算布局等
})
onDeactivated(() => {
console.log('组件停用(进入缓存)')
// 适合暂停轮询、保存临时状态、清除事件等
})
</script>
理解这四个生命周期的差异至关重要:
| 生命周期 | 触发时机 | 使用场景 |
|---|---|---|
mounted | 组件首次创建时 | 一次性的初始化,如数据获取 |
unmounted | 组件从缓存中移除时 | 最终的清理工作 |
activated | 每次从缓存进入页面 | 恢复活动状态,如开始轮询 |
deactivated | 每次离开页面进入缓存 | 暂停活动状态,如停止轮询 |
这是一个很常见问题:首次获取数据时,到底是应该在 mounted 中获取,还是在 activated 中获取?
<script setup>
import { ref, onMounted, onActivated } from 'vue'
const data = ref(null)
// 如果只放在 mounted,切换回来不会重新获取
onMounted(async () => {
data.value = await fetchData()
})
// 放在 activated,每次激活都会获取
onActivated(async () => {
data.value = await fetchData()
})
// 最佳实践:区分一次性和重复性逻辑
onMounted(() => {
console.log('组件初始化,只执行一次')
})
onActivated(async () => {
// 需要每次都刷新的数据放在这里
data.value = await fetchData()
// 恢复滚动位置
restoreScrollPosition()
})
onDeactivated(() => {
// 保存当前状态
saveScrollPosition()
// 停止不必要的轮询
stopPolling()
})
</script>
为了避免缓存过多组件导致内存溢出,我们可以设置 max 属性进行控制:
<template>
<!-- 最多缓存 10 个组件实例 -->
<keep-alive :max="10">
<router-view />
</keep-alive>
</template>
内存占用估算:
有时候我们也需要根据路由配置决定是否缓存:
<template>
<router-view v-slot="{ Component, route }">
<keep-alive v-if="route.meta.keepAlive">
<component :is="Component" />
</keep-alive>
<component :is="Component" v-else />
</router-view>
</template>
<script setup>
import { watch } from 'vue'
import { useRoute } from 'vue-router'
const route = useRoute()
// 动态设置页面标题
watch(() => route.meta.title, (title) => {
document.title = title || '默认标题'
})
</script>
路由配置:
const routes = [
{
path: '/dashboard',
name: 'Dashboard',
component: Dashboard,
meta: {
keepAlive: true, // 需要缓存
title: '仪表盘'
}
},
{
path: '/login',
name: 'Login',
component: Login,
meta: {
keepAlive: false, // 不需要缓存
title: '登录'
}
}
]
有时候也需要主动清除某个组件的缓存(如用户登出后):
<script setup>
import { ref } from 'vue'
import { useRouter } from 'vue-router'
const router = useRouter()
const componentKey = ref(0)
// 方法1:通过改变 key 强制重新渲染
function forceRerender() {
componentKey.value++
}
// 方法2:通过路由钩子清除特定组件的缓存
function clearComponentCache(componentName) {
// 需要访问 keep-alive 实例
const keepAlive = document.querySelector('keep-alive')?.__vueParentComponent
if (keepAlive) {
const cache = keepAlive.ctx.cache
const keys = keepAlive.ctx.keys
// 根据组件名称删除缓存
for (const [key, vnode] of cache.entries()) {
if (vnode.type.name === componentName) {
cache.delete(key)
keys.delete(key)
break
}
}
}
}
// 方法3:登出时清除所有缓存
function logout() {
// 清除用户数据
clearUserStore()
// 强制刷新 keep-alive
forceRerender()
// 跳转到登录页
router.push('/login')
}
</script>
<template>
<keep-alive>
<component :is="currentComponent" :key="componentKey" />
</keep-alive>
</template>
当组件被缓存后,不会重新执行数据获取逻辑。因此当切换回缓存的页面时,显示的是依然是旧数据。
onActivated(() => {
fetchData() // 每次激活都重新获取
})
onActivated(() => {
isActive.value = true
})
onDeactivated(() => {
isActive.value = false
})
watch(isActive, (active) => {
if (active) {
fetchData()
}
})
由于缓存了太多组件实例,长时间使用后导致页面变卡,内存占用持续增长:
<keep-alive :max="10">
onDeactivated(() => {
// 清理大对象
largeData.value = null
// 取消网络请求
abortController?.abort()
// 移除事件
window.removeEventListener('scroll', scrollHandler)
})
const bigList = shallowRef([]) // 只追踪引用变化,不追踪内部变化
keep-alive 只缓存直接子组件,不会穿透嵌套,因此当存在嵌套路由时,嵌套路由的组件不会被被正确缓存。
<template>
<div class="parent">
<h2>父组件</h2>
<keep-alive>
<router-view /> <!-- 缓存子路由组件 -->
</keep-alive>
</div>
</template>
<template>
<keep-alive>
<router-view v-slot="{ Component }">
<component :is="Component" />
</router-view>
</keep-alive>
</template>
<!-- 子组件内也可以有自己的 keep-alive -->
<template>
<div class="child">
<keep-alive>
<component :is="currentTab" />
</keep-alive>
</div>
</template>
由于组件没有设置 name 选项,因此即使设置了 include/exclude,但组件还是被缓存/不被缓存
<!-- 选项式 API -->
<script>
export default {
name: 'UserProfile', // 必须设置
// ...
}
</script>
<!-- 组合式 API -->
<script setup>
defineOptions({
name: 'UserProfile' // Vue 3.3+ 支持
})
// 或者使用文件名作为组件名(需要配置)
// 如果文件名为 UserProfile.vue,某些构建工具会自动设置 name
</script>
keep-alive 是一个强大的优化工具,但它不是银弹。在决定使用之前,我们需要问自己几个问题:
max 限制,避免无限增长deactivated 中释放不需要的资源keep-alive 的核心价值是在用户体验和系统资源之间找到平衡点,如果用得好,它就是性能优化的利器;如果用得不好,它可能成为内存泄漏的源头!
对于文章中错误的地方或有任何疑问,欢迎在评论区留言讨论!
156.1MB · 2026-03-12
31.0MB · 2026-03-12
117.49M · 2026-03-12