一、前言
多 Tab 页面是后台系统的标配:帖子、收藏、订单几个面板来回切。用 v-if/v-show 能实现,但代码散;Vue 有专门的组合技:<component :is> 动态组件 + keep-alive 缓存。今天还顺手解决一个真实痛点------Tab 切走再切回,用户填了一半的表单还在不在?
二、动态组件:一个标签切换 N 个组件
先看常规写法:三个 Tab 三个 v-if:
vue
<template>
<TabPost v-if="tab === 'post'" />
<TabStar v-else-if="tab === 'star'" />
<TabOrder v-else />
</template>
能跑,但每加一个 Tab 要多一行分支。动态组件一个标签搞定:
vue
<!-- TabPost.vue -->
<template>
<div>
<h4>📝 我的帖子</h4>
<input v-model="draft" placeholder="写帖草稿(切换后试试还在不在)" />
</div>
</template>
<script setup>
import { ref } from 'vue'
const draft = ref('')
</script>
vue
<!-- TabStar.vue -->
<template>
<div>
<h4>⭐ 我的收藏</h4>
<input v-model="draft" placeholder="收藏备注(切换后试试还在不在)" />
</div>
</template>
<script setup>
import { ref } from 'vue'
const draft = ref('')
</script>
vue
<!-- App.vue -->
<template>
<nav>
<button
v-for="t in tabs"
:key="t.id"
:class="{ active: tab === t.id }"
@click="tab = t.id"
>
{{ t.label }}
</button>
</nav>
<!-- 动态组件::is 是谁就渲染谁 -->
<component :is="currentComp" />
</template>
<script setup>
import { ref, computed } from 'vue'
import TabPost from './components/TabPost.vue'
import TabStar from './components/TabStar.vue'
const tabs = [
{ id: 'post', label: '帖子', comp: TabPost },
{ id: 'star', label: '收藏', comp: TabStar }
]
const tab = ref('post')
// 用 computed 从普通数组里取组件对象,避免响应式包装组件
const currentComp = computed(() => tabs.find(t => t.id === tab.value).comp)
</script>
<style scoped>
.active { background: #42b983; color: #fff; }
</style>
三个实现细节(都和"防 bug"有关):
- :is 接的是"组件对象",不是标签字符串(接字符串要求组件已全局注册)
- tabs 数组故意是普通数组不包 ref------组件对象被响应式包装后会触发 Vue 警告(组件本身不该是响应式数据)
- 切换是卸载重建:切走时 TabPost 真实销毁(触发 onUnmounted),切回来重新创建
第 3 点正是问题所在:在"帖子"里草稿打到一半,切到"收藏"再切回来------草稿没了。
【截图位置:草稿输入后切换 Tab 再切回,内容丢失的效果】
三、keep-alive:给组件加"复活甲"
<KeepAlive> 包住动态组件,切走的组件不销毁,只是被缓存起来,切回时原样复活:
vue
<template>
<nav>......同上......</nav>
<!-- 加一层 KeepAlive:组件实例被缓存,状态不丢 -->
<KeepAlive>
<component :is="currentComp" />
</KeepAlive>
</template>
再试一遍:草稿打到一半切走、切回------草稿还在。KeepAlive 把组件实例连同它的全部状态一起存进了缓存池。
三个常用属性(都是可选):
vue
<!-- 只缓存指定组件(匹配组件名) -->
<KeepAlive include="TabPost">
<component :is="currentComp" />
</KeepAlive>
<!-- 排除指定组件 -->
<KeepAlive exclude="TabStar">...</KeepAlive>
<!-- 最多缓存 10 个实例,超出按 LRU 淘汰最早的 -->
<KeepAlive :max="10">...</KeepAlive>
注意 include/exclude 匹配的是组件名 :<script setup> 组件默认用文件名当名字(TabPost.vue → TabPost);想显式指定就用 3.3+ 的 defineOptions:
js
defineOptions({ name: 'TabPost' })
四、缓存后的新生命周期:onActivated / onDeactivated
组件被缓存后,mounted/unmounted 不再反复触发。Vue 为缓存组件准备了两个专属钩子:
vue
<!-- TabPost.vue 补充 -->
<script setup>
import { ref, onActivated, onDeactivated, onMounted, onUnmounted } from 'vue'
const draft = ref('')
onMounted(() => console.log('TabPost 挂载(只有首次)'))
onUnmounted(() => console.log('TabPost 销毁(keep-alive 下切走不再触发)'))
// 被"激活":切回来时触发(首次也在 mounted 之后触发一次)
onActivated(() => {
console.log('TabPost 激活:可以刷新列表数据')
})
// 被"钝化":切走时触发(代替 unmounted)
onDeactivated(() => {
console.log('TabPost 钝化:可以暂停轮询')
})
</script>
实战含义:
- onActivated:切回 Tab 时刷新列表("我离开时别人可能改了数据")
- onDeactivated:暂停定时器/轮询(011 篇"开与关成对"铁律的缓存版:定时器在 onMounted 开、onDeactivated 停、onActivated 续)
五、选型总结:v-if / v-show / 动态组件 / keep-alive
| 方案 | 适用场景 | 状态是否保留 |
|---|---|---|
| v-if / v-else 链 | 分支少、结构差异大 | ❌ 销毁重建 |
| v-show | 同一位置高频显隐 | ✅(DOM 一直在) |
<component :is> |
多组件按条件切换,无需保状态 | ❌ 卸载重建 |
| KeepAlive + 动态组件 | 多 Tab 且要保状态 | ✅(实例缓存) |
口诀:要保状态上 KeepAlive,切回来要刷新用 onActivated。
六、综合实战:后台 Tab 容器雏形
把三态(帖子/收藏/订单)+ 缓存 + 激活刷新 + 上限控制合成一版:
vue
<!-- App.vue -->
<template>
<nav>
<button
v-for="t in tabs"
:key="t.id"
:class="{ active: tab === t.id }"
@click="tab = t.id"
>
{{ t.label }}
</button>
</nav>
<KeepAlive :max="5">
<component :is="currentComp" />
</KeepAlive>
</template>
<script setup>
import { ref, computed } from 'vue'
import TabPost from './components/TabPost.vue'
import TabStar from './components/TabStar.vue'
const tabs = [
{ id: 'post', label: '帖子', comp: TabPost },
{ id: 'star', label: '收藏', comp: TabStar }
]
const tab = ref('post')
const currentComp = computed(() => tabs.find(t => t.id === tab.value).comp)
</script>
再加一个 Tab 只需要:写新组件文件 + tabs 数组加一行。路由篇(045 篇)的 keep-alive 页面缓存,本质就是今天这套东西的 router-view 版。
【截图位置:开启缓存后草稿保留、控制台 onActivated 日志的效果】
七、踩坑记录
- :is 绑了带响应式的组件对象 :
ref(TabPost)或把组件放 reactive 里,控制台警告 "Vue received a Component that was made a reactive object"------用普通对象存储或 markRaw 包组件 - include 不生效:匹配的是组件名,检查文件名或 defineOptions({ name }) 是否一致
- 缓存组件里 onUnmounted 不触发:被缓存后切走走的是 onDeactivated------清理定时器的代码要放对钩子,否则"切走了定时器还在跑"
- 缓存太久数据陈旧:不是 bug,是特性------要新鲜数据在 onActivated 里重新拉取
- KeepAlive 里套 v-if 做条件渲染 :
<KeepAlive><component v-if="show" :is="comp" /></KeepAlive>可以工作,但条件切换会导致缓存实例被清,理解行为再用
八、今日小结
<component :is="组件对象" />一标签切换多组件,配合普通数组/computed 取组件防响应式警告- KeepAlive 缓存组件实例:include/exclude/max 精细控制,匹配组件名
- 缓存组件专属钩子:onActivated(切回刷新)/ onDeactivated(切走收尾)
- 保状态四方案对比记牢,口诀:保状态上 KeepAlive
下篇预告
弹窗挂在组件树深处,被父级的 overflow:hidden 裁掉、被 z-index 压在下面怎么办?下一篇 031 Teleport 传送门------让弹窗"物理搬家"到 body 下,CSS 战场瞬间清净。