Vue.js: 动态与异步组件
最后更新:2026-10-20
动态组件让你用 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 切换的后台仪表盘:
产品经理 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) |
| 主题切换(不同布局) | ✅ |
▶ 示例:一个 <component :is> 切换 3 个面板
▶ 试一试
输出:
TEXT
初始「首页」按钮绿底白字高亮,下方虚线框显示「首页:今日待办 3 条,未读消息 5 条」。
点「个人资料」→ 高亮跟着移过去,面板内容整体换成「个人资料:Alice / 管理员 / 已登录 12 天」。
再点「设置」→ 再换成设置文案;三块面板共用同一个 `<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"。
(2) 2 个插槽
(3) 5 大优势
| 优势 | 说明 |
|---|---|
| 简洁 | 不用手写 loading 状态 |
| 统一 | 多个异步组件统一处理 |
| 嵌套 | 支持嵌套 Suspense |
| 错误处理 | 配合 onErrorCaptured |
| SSR 友好 | 服务端自动等待 |
6. 动态组件 + keep-alive 缓存
(1) 默认行为:切换组件会销毁/重建
(2) keep-alive 缓存(避免重复创建)
(3) keep-alive 3 大配置
▶ 示例:动态 Tab + keep-alive 缓存
▶ 展开运行示例(69 行)
输出:
TEXT
三个按钮:Home 绿色(当前选中)、Profile、Settings,下方浅绿面板显示「Home」、按钮「计数: 0」和灰色提示文字。
点计数 → 1;切到 Profile(蓝底,显示「用户资料」)再切回 Home,计数仍是 1。
控制台随切换打印 xxx mounted / activated / deactivated。
7. 综合演练:Suspense 处理异步加载
▶ 示例:Suspense 处理异步加载
▶ 试一试输出:
TEXT
初始只有一个按钮「加载 异步组件」,页面其余部分空白。
点击后按钮变「卸载 异步组件」,先出现黄色「⏳ Loading...」,约 1.5 秒后替换为浅绿面板:标题「AsyncHeavy」和「加载完成于: 当前时间」。
再点按钮 → 面板消失,按钮回到「加载 异步组件」。
⚠️ 真实项目用
defineAsyncComponent(() => import('./Chart.vue'))。这里用 async setup() 模拟延迟。
(1) 📊 5 种 :is 写法对比
| 写法 | 示例 | 适用 |
|---|---|---|
| 字符串 | <component :is="'HomeTab'"> |
全局注册(不推荐) |
| 组件对象 | <component :is="HomeTab"> |
静态导入(最常用) |
| 异步组件对象 | <component :is="AsyncComponent"> |
按需加载 |
| 计算属性 | <component :is="currentTabComponent"> |
动态切换 |
| 内联对象 | <component :is="{ template: '...' }"> |
简单场景 |
(2) 📊 5 个常见错误速查
| 错误 | 现象 | 解决 |
|---|---|---|
| :is 字符串但未全局注册 | 警告 Unknown component | 用组件对象或先注册 |
| 异步组件没 fallback | 加载时空白 | 用 Suspense 包裹 |
| 切换组件重新加载数据 | 性能差 | 用 keep-alive 缓存 |
| 异步组件报错未处理 | 静默失败 | 配合 onErrorCaptured |
| 动态组件忘加 :key | 状态错乱 | 加 :key="tab.name" |
(3) 📊 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 强类型动态组件