Vue.js: 动态与异步组件
最后更新:2026-08-26
动态组件让你用 1 个 <component> 标签动态渲染不同的组件——根据数据切换 Tab、Modal、路由视图。异步组件让你按需加载组件——首屏只加载必要的代码,提升性能。
Vue 3 的 <Suspense> 实验性组件专门处理异步组件的 loading 状态,告别手写 v-if="loading"。本课帮你掌握 3 个核心 API。
1. 你将学到
<component :is="...">动态组件基础- 字符串 / 组件对象 2 种
:is写法 defineAsyncComponent异步加载- 代码分包(动态 import)的性能优势
<Suspense>处理异步组件- 动态组件 + keep-alive 缓存
- 5 个实战场景和 3 个反模式
2. 一个 Tab 切换的 5 个 v-if 噩梦
(1) 痛点:5 个 Tab,10 个 v-if 嵌套
Alice 构建了一个带 Tab 切换的后台仪表盘:
VUE
<!-- ❌ 翻车版:5 个 Tab 用 5 个 v-if -->
<template>
<div class="tabs">
<button @click="currentTab = 'home'">Home</button>
<button @click="currentTab = 'profile'">Profile</button>
<button @click="currentTab = 'settings'">Settings</button>
<div v-if="currentTab === 'home'">
<HomeTab />
</div>
<div v-else-if="currentTab === 'profile'">
<ProfileTab />
</div>
<div v-else-if="currentTab === 'settings'">
<SettingsTab />
</div>
</div>
</template>
产品经理 Charlie:
"Alice,还要加 5 个 Tab。10 行 v-if-else 根本没法维护。用动态组件吧。"
(2) Vue 动态组件解法:1 个 <component> 切换 5 个 Tab
VUE
<!-- ✅ 正确版:1 个 <component> 切换 -->
<template>
<div class="tabs">
<button
v-for="tab in tabs"
:key="tab.name"
:class="{ active: currentTab === tab.name }"
@click="currentTab = tab.name"
>
{{ tab.label }}
</button>
<!-- ✅ 1 个 <component> 标签动态切换 -->
<component :is="currentTabComponent" />
</div>
</template>
<script setup>
const { ref, computed, defineAsyncComponent } = Vue
// 同步组件
import HomeTab from './tabs/HomeTab.vue'
import ProfileTab from './tabs/ProfileTab.vue'
// 异步组件(按需加载)
const SettingsTab = defineAsyncComponent(() => import('./tabs/SettingsTab.vue'))
const tabs = [
{ name: 'home', label: 'Home' },
{ name: 'profile', label: 'Profile' },
{ name: 'settings', label: 'Settings' }
]
const currentTab = ref('home')
// 计算属性:根据 currentTab 决定显示哪个组件
const currentTabComponent = computed(() => {
return { home: HomeTab, profile: ProfileTab, settings: SettingsTab }[currentTab.value]
})
</script>
(3) 收益
使用动态组件后:
- 代码量:10 行 v-if → 1 行
<component>(-90%) - 可扩展:加新 Tab 只需加 1 个 import + 1 个 component 对象
- 首屏性能:SettingsTab 异步加载,节省 ~50KB
- 可维护性:5 个 Tab 在 1 个数组里管理
3. <component :is="..."> 基础
(1) 5 种 :is 写法
VUE
<!-- 写法 1:组件名字符串(全局注册过的) -->
<component :is="'HomeTab'" />
<!-- 写法 2:组件对象(最常用) -->
<component :is="HomeTab" />
<!-- 写法 3:动态计算属性 -->
<component :is="currentTabComponent" />
<!-- 写法 4:异步组件对象 -->
<component :is="asyncComponent" />
<!-- 写法 5:内联组件对象 -->
<component :is="{ template: '<div>Inline</div>' }" />
(2) 完整示例
VUE
<!-- TabContainer.vue -->
<template>
<div>
<button
v-for="tab in tabs"
:key="tab.name"
@click="currentTab = tab.name"
>
{{ tab.label }}
</button>
<!-- 动态组件 -->
<component :is="currentTabComponent" />
</div>
</template>
<script setup>
const { ref, computed } = Vue
import HomeTab from './tabs/HomeTab.vue'
import ProfileTab from './tabs/ProfileTab.vue'
import SettingsTab from './tabs/SettingsTab.vue'
const tabs = [
{ name: 'home', label: 'Home' },
{ name: 'profile', label: 'Profile' },
{ name: 'settings', label: 'Settings' }
]
const currentTab = ref('home')
const currentTabComponent = computed(() => ({
home: HomeTab,
profile: ProfileTab,
settings: SettingsTab
}[currentTab.value]))
</script>
(3) 5 大应用场景
| 场景 | 动态组件 |
|---|---|
| Tab 切换 | ✅ |
| Modal/Dialog | ✅ |
| 多步骤表单 | ✅ |
| 路由视图 | ✅(<router-view> 内部用 component) |
| 主题切换(不同布局) | ✅ |
4. 异步组件 defineAsyncComponent
(1) 为什么需要异步组件?
普通组件在 import 时立即加载所有代码。大组件(如富文本编辑器、图表库)首屏不必要,但会阻塞加载。异步组件按需加载,首屏更快。
VUE
<script setup>
const { defineAsyncComponent } = Vue
import LoadingSpinner from './LoadingSpinner.vue'
// 异步组件:只有使用 ChartEditor 时才加载它的代码
const ChartEditor = defineAsyncComponent({
// 1. 加载函数
loader: () => import('./ChartEditor.vue'),
// 2. 加载中显示
loadingComponent: LoadingSpinner,
// 3. 加载失败显示
errorComponent: ErrorMessage, // 需要 import ErrorMessage from './ErrorMessage.vue'
// 4. 加载延迟(避免闪烁)
delay: 200
})
</script>
<template>
<ChartEditor v-if="showEditor" />
</template>
(2) 简化写法
VUE
<script setup>
const { defineAsyncComponent } = Vue
// ✅ 简化版:只有 loader
const ChartEditor = defineAsyncComponent(() => import('./ChartEditor.vue'))
</script>
(3) 5 大异步组件场景
| 场景 | 异步加载 |
|---|---|
| 富文本编辑器 | ✅ |
| 图表库(ECharts/D3) | ✅ |
| 大表单(含 100+ 字段) | ✅ |
| Modal 弹窗 | ✅ |
| 路由懒加载 | ✅(Vue Router 自动用) |
5. <Suspense> 处理异步组件
(1) 什么是 Suspense?
Suspense 组件是 Vue 3 的异步组件 loading 协调器——自动处理异步组件的 loading 状态、错误状态。告别手写 v-if="loading"。
VUE
<template>
<!-- Suspense 自动等待异步组件加载完成 -->
<Suspense>
<!-- 异步组件 -->
<ChartEditor :data="chartData" />
<!-- 加载中显示(默认插槽) -->
<template #fallback>
<LoadingSpinner />
</template>
</Suspense>
</template>
(2) 2 个插槽
VUE
<template>
<Suspense>
<!-- 默认插槽:异步内容 -->
<AsyncComponent />
<!-- fallback 插槽:加载中显示 -->
<template #fallback>
<div>Loading...</div>
</template>
</Suspense>
</template>
(3) 5 大优势
| 优势 | 说明 |
|---|---|
| 简洁 | 不用手写 loading 状态 |
| 统一 | 多个异步组件统一处理 |
| 嵌套 | 支持嵌套 Suspense |
| 错误处理 | 配合 onErrorCaptured |
| SSR 友好 | 服务端自动等待 |
6. 动态组件 + keep-alive 缓存
(1) 默认行为:切换组件会销毁/重建
VUE
<template>
<component :is="currentTabComponent" />
</template>
<!-- 切换 Tab 时:
1. 旧组件 onBeforeUnmount
2. 旧组件 onUnmounted
3. 新组件 onBeforeMount
4. 新组件 onMounted
5. 每次切换都重新加载数据(性能差)
-->
(2) keep-alive 缓存(避免重复创建)
VUE
<template>
<keep-alive>
<component :is="currentTabComponent" />
</keep-alive>
</template>
<!-- 切换 Tab 时:
1. 旧组件 onDeactivated(不停用销毁)
2. 新组件 onActivated(从缓存恢复)
3. 数据保留,滚动位置保留
-->
(3) keep-alive 3 大配置
VUE
<template>
<!-- include:只缓存指定组件 -->
<keep-alive include="HomeTab,ProfileTab">
<component :is="currentTabComponent" />
</keep-alive>
<!-- exclude:不缓存指定组件 -->
<keep-alive exclude="SettingsTab">
<component :is="currentTabComponent" />
</keep-alive>
<!-- max:最多缓存 5 个 -->
<keep-alive :max="5">
<component :is="currentTabComponent" />
</keep-alive>
</template>
7. 完整示例:动态 Tab + 异步加载
▶ 示例:动态 Tab + keep-alive 缓存
HTML
📖 仅展示
<script src="https://unpkg.com/vue@3/dist/vue.global.prod.js"></script>
<style>
button { padding: 6px 12px; margin: 2px; border: none; background: #eee; cursor: pointer; border-radius: 4px; }
button.active { background: #42b883; color: white; }
.tab-content { padding: 1rem; border: 1px solid #ddd; margin-top: 0.5rem; min-height: 60px; }
</style>
<div id="app">
<button
v-for="tab in tabs" :key="tab.name"
:class="{ active: currentTab === tab.name }"
@click="currentTab = tab.name"
>{{ tab.label }}</button>
<keep-alive :max="3">
<component :is="currentTabComponent" :key="currentTab"></component>
</keep-alive>
<p style="color: #999; font-size: 0.85rem;">查看 console(切换 Tab 看激活/停用)</p>
</div>
<script>
const { createApp, ref, computed, onMounted, onActivated, onDeactivated } = Vue
// 3 个 Tab 组件
const Home = {
setup() {
onMounted(() => console.log('🏠 Home mounted'))
onActivated(() => console.log('🏠 Home activated'))
onDeactivated(() => console.log('🏠 Home deactivated'))
return { counter: ref(0) }
},
template: `
<div class="tab-content" style="background: #ecfdf5;">
<h3>Home</h3>
<button @click="$data.counter = ($data.counter || 0) + 1">计数: {{ $data.counter || 0 }}</button>
<p style="font-size: 0.85rem; color: #999;">切走再切回,计数保留(keep-alive 缓存)</p>
</div>
`
}
const Profile = {
setup() {
onMounted(() => console.log('👤 Profile mounted'))
onActivated(() => console.log('👤 Profile activated'))
onDeactivated(() => console.log('👤 Profile deactivated'))
return {}
},
template: '<div class="tab-content" style="background: #eff6ff;"><h3>Profile</h3><p>用户资料</p></div>'
}
const Settings = {
setup() {
onMounted(() => console.log('⚙️ Settings mounted'))
onActivated(() => console.log('⚙️ Settings activated'))
onDeactivated(() => console.log('⚙️ Settings deactivated'))
return {}
},
template: '<div class="tab-content" style="background: #fef3c7;"><h3>Settings</h3><p>系统设置</p></div>'
}
const App = {
components: { Home, Profile, Settings },
setup() {
const tabs = [
{ name: 'home', label: 'Home' },
{ name: 'profile', label: 'Profile' },
{ name: 'settings', label: 'Settings' }
]
const currentTab = ref('home')
const currentTabComponent = computed(() =>
({ home: Home, profile: Profile, settings: Settings })[currentTab.value]
)
return { tabs, currentTab, currentTabComponent }
}
}
createApp(App).mount('#app')
</script>
▶ 示例:Suspense 处理异步加载
HTML
<script src="https://unpkg.com/vue@3/dist/vue.global.prod.js"></script>
<div id="app">
<button @click="show = !show">{{ show ? '卸载' : '加载' }} 异步组件</button>
<suspense v-if="show">
<!-- 模拟"异步组件":返回一个 Promise -->
<async-heavy></async-heavy>
<template #fallback>
<div style="padding: 1rem; background: #fef3c7;">⏳ Loading...</div>
</template>
</suspense>
</div>
<script>
const { createApp, ref, defineAsyncComponent } = Vue
// 模拟异步组件:返回 Promise 的 setup(Suspense 会等待)
const AsyncHeavy = {
async setup() {
await new Promise(r => setTimeout(r, 1500)) // 模拟 1.5s 加载
return { time: new Date().toLocaleTimeString() }
},
template: '<div style="padding: 1rem; background: #ecfdf5;"><h3>AsyncHeavy</h3><p>加载完成于: {{ time }}</p></div>'
}
const App = {
components: { AsyncHeavy },
setup() {
const show = ref(false)
return { show }
}
}
createApp(App).mount('#app')
</script>
⚠️ 真实项目用
defineAsyncComponent(() => import('./Chart.vue'))。这里用 async setup() 模拟延迟。
▶ 示例:5 种 :is 写法对比
| 写法 | 示例 | 适用 |
|---|---|---|
| 字符串 | <component :is="'HomeTab'"> |
全局注册(不推荐) |
| 组件对象 | <component :is="HomeTab"> |
静态导入(最常用) |
| 异步组件对象 | <component :is="AsyncComponent"> |
按需加载 |
| 计算属性 | <component :is="currentTabComponent"> |
动态切换 |
| 内联对象 | <component :is="{ template: '...' }"> |
简单场景 |
▶ 示例:5 个常见错误速查
| 错误 | 现象 | 解决 |
|---|---|---|
| :is 字符串但未全局注册 | 警告 Unknown component | 用组件对象或先注册 |
| 异步组件没 fallback | 加载时空白 | 用 Suspense 包裹 |
| 切换组件重新加载数据 | 性能差 | 用 keep-alive 缓存 |
| 异步组件报错未处理 | 静默失败 | 配合 onErrorCaptured |
| 动态组件忘加 :key | 状态错乱 | 加 :key="tab.name" |
▶ 示例:5 大性能对比
| 模式 | 首屏 | 切换 | 适用 |
|---|---|---|---|
| 直接 import | 慢 | 重新加载 | 小组件 |
| 动态 import | 快 | 首次切换慢 | 大组件 |
| Suspense | 快 | 平滑 | 用户体验优先 |
| keep-alive | 快 | 0 重新加载 | 频繁切换 |
| 异步 + keep-alive | ⭐⭐⭐⭐⭐ | ⭐⭐⭐⭐⭐ | 最佳实践 |
❓ 常见问题
Q 动态组件和 v-if 区别?
A v-if 5 个判断 5 个分支,组件都加载。动态组件 1 个
<component> 标签,根据 :is 切换,更简洁。异步组件还能按需加载。Q defineAsyncComponent 必须在 setup 顶层吗?
A 是的。
defineAsyncComponent(() => import('...')) 必须在 <script setup> 顶层调用(不能在函数内)。Q Suspense 是稳定特性吗?
A Vue 3.2+ 已稳定。可以放心用,但生产环境建议测试好 fallback 行为。
Q 动态组件能传 props 吗?
A 能。直接用 v-bind 传:
<component :is="Comp" :prop1="x" :prop2="y" />。Q 异步组件失败怎么捕获?
A 用
errorComponent 选项 + onErrorCaptured:{ loader, errorComponent: ErrorComp }。Suspense 内可用 onErrorCaptured 包裹。Q keep-alive 缓存的组件数据会持久化吗?
A 缓存期间数据保留(不销毁)。但页面刷新或路由切换会清除缓存。需要持久化用 Pinia。
Q defineAsyncComponent 和 Vue Router 懒加载什么关系?
A Vue Router 内部就用
defineAsyncComponent 实现路由懒加载。本质是一回事。📖 小节
- 动态组件:
<component :is="...">1 个标签切换多个组件 - 5 种 :is 写法:字符串 / 组件对象 / 异步 / 计算属性 / 内联
- 异步组件
defineAsyncComponent:按需加载,首屏快 - Suspense:自动处理异步组件 loading 状态
- keep-alive + 动态组件:缓存避免重复创建
- 5 大场景:Tab / Modal / 路由 / 多步表单 / 主题
- 性能最佳实践:异步 + keep-alive + Suspense
📝 作业
-
基础题(难度⭐) 实现一个简单的 Tab 切换:
- 3 个 Tab(Home/Profile/Settings)
- 用
<component :is>动态切换 - 切换时输出 "Component X created/destroyed"
-
进阶题(难度⭐⭐) 实现 Tab + 异步加载:
- 3 个 Tab,SettingsTab 用
defineAsyncComponent异步加载 - 显示加载状态(手写 v-if loading)
- 切回 HomeTab 时不重新加载
- 3 个 Tab,SettingsTab 用
-
挑战题(难度⭐⭐⭐) 实现完整的动态 + 异步 + 缓存系统:
- 5 个 Tab,至少 2 个异步加载
- 用
<Suspense>包裹 <keep-alive :max="3">缓存- 切换时输出 onActivated/onDeactivated
- 异步组件失败时显示 ErrorComponent
- 测量首屏加载时间(before/after 异步)
- TypeScript 强类型动态组件