Vue.js: 动态与异步组件

最后更新:2026-08-26

动态组件让你用 1 个 <component> 标签动态渲染不同的组件——根据数据切换 Tab、Modal、路由视图。异步组件让你按需加载组件——首屏只加载必要的代码,提升性能。

Vue 3 的 <Suspense> 实验性组件专门处理异步组件的 loading 状态,告别手写 v-if="loading"。本课帮你掌握 3 个核心 API。

1. 你将学到


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) 收益

使用动态组件后:


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>
逻辑代码 69 行(超过 40 行限制,仅展示)

▶ 示例: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 异步组件失败怎么捕获?
AerrorComponent 选项 + onErrorCaptured:{ loader, errorComponent: ErrorComp }。Suspense 内可用 onErrorCaptured 包裹。
Q keep-alive 缓存的组件数据会持久化吗?
A 缓存期间数据保留(不销毁)。但页面刷新或路由切换会清除缓存。需要持久化用 Pinia。
Q defineAsyncComponent 和 Vue Router 懒加载什么关系?
A Vue Router 内部就用 defineAsyncComponent 实现路由懒加载。本质是一回事。

📖 小节


📝 作业

  1. 基础题(难度⭐) 实现一个简单的 Tab 切换:

    • 3 个 Tab(Home/Profile/Settings)
    • <component :is> 动态切换
    • 切换时输出 "Component X created/destroyed"
  2. 进阶题(难度⭐⭐) 实现 Tab + 异步加载:

    • 3 个 Tab,SettingsTab 用 defineAsyncComponent 异步加载
    • 显示加载状态(手写 v-if loading)
    • 切回 HomeTab 时不重新加载
  3. 挑战题(难度⭐⭐⭐) 实现完整的动态 + 异步 + 缓存系统:

    1. 5 个 Tab,至少 2 个异步加载
    2. <Suspense> 包裹
    3. <keep-alive :max="3"> 缓存
    4. 切换时输出 onActivated/onDeactivated
    5. 异步组件失败时显示 ErrorComponent
    6. 测量首屏加载时间(before/after 异步)
    7. TypeScript 强类型动态组件
Web-Tutorial.com

Web-Tutorial 技术团队

由多位开发者共同维护的编程教程平台。每篇教程由对应领域的开发者编写和审核,确保内容准确可靠。如发现任何问题,欢迎向我们反馈。

100%

🙏 帮我们做得更好

我们是刚上线的编程教程站,几个人的小团队,精力有限。页面虽经检查,难免还有疏漏——链接失效、排版错乱、内容有误、语言生硬……

如果您发现了,麻烦告诉我们,我们会在收到反馈后第一时间进行修复,再次感谢您的光临 🙏