技术知识文章集合TECHNICAL ARCHIVE · 457 DOCUMENTS

显示模式

登录
ARCHIVE DOCUMENTVUE

Vue 之动态组件和异步组件

所属馆藏
Vue
文件格式
Markdown
原始路径
Vue/41-Vue之动态组件和异步组件
本文目录12 个章节
  1. 一、动态组件是什么
  2. 二、动态组件切换时会发生什么
  3. 三、使用 KeepAlive 保留组件状态
  4. 四、异步组件是什么
  5. 五、Vue 3.5 的 defineAsyncComponent
  6. 六、Vue 2.7 的异步组件写法
  7. 七、Vue 2.7 与 Vue 3.5 的异步组件版本对照
  8. 八、路由级懒加载
  9. 九、v-if、v-show 与异步组件
  10. 十、总结
  11. 官方参考
  12. 原文出处

Vue 之动态组件和异步组件

动态组件解决“当前要显示哪个组件”的问题;异步组件解决“组件代码什么时候下载和解析”的问题。两者可以组合,但不是同一个概念。

本文保留原文的动态组件、keep-alive、异步加载和懒加载示例,并按 Vue 2.7.16、Vue 3.5.39 的 API 重新整理。

一、动态组件是什么

动态组件是根据状态切换组件,例如在“文章列表”和“文章归档”之间切换:

<script setup lang="ts">
import { ref } from 'vue'
import Posts from './Posts.vue'
import Archive from './Archive.vue'

const current = ref<'posts' | 'archive'>('posts')
const views = {
  posts: Posts,
  archive: Archive,
}
</script>

<template>
  <button @click="current = 'posts'">文章</button>
  <button @click="current = 'archive'">归档</button>

  <component :is="views[current]" />
</template>

<component> 是 Vue 的动态组件占位符,:is 可以接收:

  • 已注册组件的字符串名称,例如 :is="currentComponentName"
  • 导入的组件对象,例如上例的 views[current]
  • 原生元素名称,例如 :is="'div'",但实际项目中要谨慎使用。

Vue 2 的对应写法是:

<component :is="currentTabComponent"></component>

参考:Vue 2 动态组件Vue 3 动态组件

Vue 2 的完整切换示例

原文使用了运行时模板和全局注册组件的方式,思路可以保留。下面是格式化后的 Vue 2 示例:

Vue.component('input-item', {
  template: '<input />',
})

Vue.component('common-item', {
  template: '<div>hello world</div>',
})

new Vue({
  el: '#app',
  data() {
    return {
      currentItem: 'input-item',
    }
  },
  methods: {
    handleClick() {
      this.currentItem = this.currentItem === 'input-item'
        ? 'common-item'
        : 'input-item'
    },
  },
  template: `
    <div>
      <component :is="currentItem"></component>
      <button @click="handleClick">切换</button>
    </div>
  `,
})

template 字符串需要 Vue 的 runtime compiler。使用 Vue CLI、Vite 或 Nuxt 的 SFC 项目通常不写这种运行时模板,而是直接创建 .vue 文件。

二、动态组件切换时会发生什么

默认情况下,动态组件切换会卸载旧组件并创建新组件:

当前是 Posts
    ↓ current 改为 Archive
卸载 Posts,创建 Archive

因此,如果 Posts 中有输入框、分页位置或临时状态,切换回来时可能重新初始化。这里要区分两件事:

  • 动态组件决定显示哪一个组件;
  • KeepAlive决定不显示的组件实例是否进入缓存而不是被卸载。

原文中“切换组件时值会被清掉,所以需要使用 keep-alive”的意图是正确的,但原句容易读成“keep-alive 会清掉值”。实际正好相反:<KeepAlive> 会缓存符合条件的组件实例。

三、使用 KeepAlive 保留组件状态

<template>
  <KeepAlive>
    <component :is="views[current]" />
  </KeepAlive>
</template>

切换到其他组件时,当前组件会被停用并放入缓存;再次切换回来时,原实例会重新激活。输入框内容、组件本地状态和已创建的子组件实例可以保留。

KeepAlive 生命周期

import {
  onActivated,
  onDeactivated,
} from 'vue'

onActivated(() => {
  // 组件首次挂载后,以及之后每次从缓存中恢复时执行
})

onDeactivated(() => {
  // 组件被放入 KeepAlive 缓存时执行
})

deactivated 不等于 unmounted。只有缓存项被 max 淘汰、父组件卸载或 KeepAlive 本身卸载时,组件才会真正卸载。Vue 2 中对应的是 activated / deactivated 选项钩子。

include、exclude 和 max

<KeepAlive
  :include="['Posts', 'Archive']"
  :exclude="/^Settings/"
  :max="10"
>
  <component :is="views[current]" />
</KeepAlive>
  • include:只有名称匹配的组件会被缓存;
  • exclude:名称匹配的组件不会被缓存;
  • max:最多缓存多少个实例,超过后会按 LRU(最近最少使用)策略淘汰;
  • include / exclude 支持逗号分隔字符串、正则表达式或数组。

名称匹配依赖组件的 name。在 Vue 3.2.34 及之后,<script setup> SFC 通常会根据文件名推断组件名,但需要过滤时仍建议使用稳定、清晰的组件名称。Vue 2 项目则应显式设置 name

<script setup lang="ts">
defineOptions({ name: 'Posts' })
</script>

KeepAlive 只缓存组件实例,不会阻止异步组件的 loader 下载;异步加载和实例缓存是两个阶段。

参考:Vue 3 KeepAliveVue 2 keep-alive API

四、异步组件是什么

异步组件把组件的加载推迟到真正需要渲染时:

应用启动
  ↓
只加载首屏需要的代码
  ↓ 用户进入某功能
下载该功能的代码分块
  ↓
创建并渲染组件

它适合:

  • 页面下方或弹窗中暂时不会展示的大型组件;
  • 管理后台中不常访问的功能模块;
  • 路由级代码分割;
  • 需要把大型 JavaScript 拆成多个 chunk 的应用。

异步组件不是“4 秒后用 setTimeout 显示文本”。setTimeout 只是原文为了模拟网络延迟而写的演示。真实项目通常通过 import() 让构建工具完成代码分割。

五、Vue 3.5 的 defineAsyncComponent

1. 最简单的写法

import { defineAsyncComponent } from 'vue'

const AsyncPanel = defineAsyncComponent(() => import('./Panel.vue'))

然后像普通组件一样使用:

<script setup lang="ts">
import AsyncPanel from './AsyncPanel'
</script>

<template>
  <AsyncPanel />
</template>

也可以在组件注册表中切换:

<script setup lang="ts">
import { ref } from 'vue'
import { defineAsyncComponent } from 'vue'
import LocalPanel from './LocalPanel.vue'

const current = ref('local')
const panels = {
  local: LocalPanel,
  remote: defineAsyncComponent(() => import('./RemotePanel.vue')),
}
</script>

<template>
  <component :is="panels[current]" />
</template>

loader 返回的 Promise 会在异步组件真正渲染时执行。组件加载结果会被 wrapper 缓存,但这不等于 KeepAlive 的组件实例缓存。

2. loading、error、delay 和 timeout

import { defineAsyncComponent } from 'vue'
import LoadingPanel from './LoadingPanel.vue'
import ErrorPanel from './ErrorPanel.vue'

const AsyncPanel = defineAsyncComponent({
  loader: () => import('./Panel.vue'),
  loadingComponent: LoadingPanel,
  errorComponent: ErrorPanel,
  delay: 200,
  timeout: 3000,
})

Vue 3 的选项含义:

  • loadingComponent:加载中显示的组件;
  • errorComponent:加载失败后显示的组件;
  • delay:显示 loading 前等待多久,默认 200ms,避免快速请求产生闪烁;设置为 0 可立即显示;
  • timeout:超过指定毫秒数后视为失败,默认 Infinity,也就是不主动超时;
  • suspensible:是否让父级 Suspense 接管异步状态,默认 true
  • onError:自定义重试和失败处理。

errorComponent 可以接收名为 error 的 prop:

<script setup lang="ts">
defineProps<{
  error: unknown
}>()
</script>

<template>
  <div role="alert">组件加载失败:{{ error }}</div>
</template>

3. 失败重试

const AsyncPanel = defineAsyncComponent({
  loader: () => import('./Panel.vue'),
  onError(error, retry, fail, attempts) {
    // 网络暂时失败时最多重试三次
    if (attempts <= 3) {
      retry()
    } else {
      fail()
    }
  },
})

onError 中必须明确调用 retry()fail()。不要无条件重试,否则网络断开时可能形成无限请求。

4. Suspense 与异步组件

<script setup lang="ts">
import AsyncPanel from './AsyncPanel'
</script>

<template>
  <Suspense>
    <template #default>
      <AsyncPanel />
    </template>

    <template #fallback>
      <div class="loading">Loading...</div>
    </template>
  </Suspense>
</template>

Suspense 可以等待异步组件和带有顶层 await 的异步 setup()。当异步组件处于默认的 suspensible: true 状态并位于 Suspense 内部时,wrapper 会把 loader 注册为 Suspense 依赖,未完成期间由 Suspense 的 fallback/pending 状态控制展示;异步 wrapper 自己的 loading、delay 和 timeout 分支不会按普通方式驱动 UI。loader reject 仍应通过 onErrorerrorCaptured/onErrorCaptured 处理,Suspense 本身没有 error slot;不要把 fallback 当成错误组件。

如果希望由异步组件 wrapper 自己管理 loading/delay/timeout 状态,可以设置:

const AsyncPanel = defineAsyncComponent({
  loader: () => import('./Panel.vue'),
  loadingComponent: LoadingPanel,
  errorComponent: ErrorPanel,
  suspensible: false,
})

Vue 3.5.39 的 <Suspense> 仍标注为 Experimental。它提供 pendingresolvefallback 等事件,但没有一个可以替代 errorCaptured 的 error slot。异步错误可以通过 onError、父组件的 onErrorCaptured 或应用级 app.config.errorHandler 处理。

import { onErrorCaptured } from 'vue'

onErrorCaptured(error => {
  console.error('异步组件错误', error)
  return false
})

不要使用原文中的 lazy()lazy 不是 Vue 的通用内置函数;代码分割应使用 import(),组件加载状态应使用 defineAsyncComponentSuspense 或业务组件。

参考:Vue 3 Async ComponentsVue 3 Suspense

六、Vue 2.7 的异步组件写法

Vue 2 的异步组件主要有三类历史写法:factory callback、返回 Promise 的 factory,以及带 loading/error 配置的高级 factory。

1. factory callback

Vue.component('async-panel', function (resolve, reject) {
  require(['./components/Panel.vue'], resolve)
})

这种 require 是 webpack 等构建工具提供的异步模块语法,不是浏览器原生 API。它的核心是:组件加载成功后调用 resolve,失败时调用 reject

2. Promise factory

Vue.component('async-panel', () => import('./components/Panel.vue'))

这也是 Vue 2 中常见的代码分割写法。Vue 2.7 仍支持,但新 Vue 3 项目应优先写成 defineAsyncComponent,因为 Vue 3 的异步组件状态和 Suspense 集成方式不同。

3. loading/error 高级 factory

const LoadingPanel = {
  template: '<div>loading...</div>',
}

const ErrorPanel = {
  template: '<div>加载失败</div>',
}

const AsyncPanel = () => ({
  component: import('./components/Panel.vue'),
  loading: LoadingPanel,
  error: ErrorPanel,
  delay: 200,
  timeout: 3000,
})

Vue.component('async-panel', AsyncPanel)

Vue 2 的字段名是 componentloadingerrordelaytimeout;Vue 3 对应的对象选项是 loaderloadingComponenterrorComponent,不能只改名字而不说明版本。

Vue 2 文档中这类高级 factory 的 loading 默认延时为 200ms,timeout 不设置时不主动超时。参考:Vue 2 异步组件 Loading 状态

七、Vue 2.7 与 Vue 3.5 的异步组件版本对照

项目Vue 2.7.16Vue 3.5.39
基础写法factory callback 或 () => import()defineAsyncComponent(() => import())
loading 选项loadingloadingComponent
error 选项errorerrorComponent
未解析状态注释 VNode 占位符由异步 wrapper、renderer 或 Suspense 管理
加载完成更新记录 owners,调用 $forceUpdate()响应式状态、pending Promise 和 wrapper 更新
Suspense不支持 Vue 3 Suspense内置但仍为 Experimental
组件 APIOptions API 为主,2.7 提供部分组合式 APIComposition API、defineAsyncComponent

Vue 2.7.4 起还提供了一个兼容 API defineAsyncComponent,但它底层仍走 Vue 2 的 async factory/placeholder 机制,不能说它返回 Vue 3 的 AsyncComponentWrapper。本文第八节会单独说明 Vue 2 源码。

八、路由级懒加载

路由组件的懒加载与普通异步组件相关,但不建议混用两层 wrapper。Vue Router 4 推荐直接写:

import { createRouter, createWebHistory } from 'vue-router'

const router = createRouter({
  history: createWebHistory(),
  routes: [
    {
      path: '/reports',
      component: () => import('./views/Reports.vue'),
    },
  ],
})

不要写成:

// 不推荐:不要把 defineAsyncComponent 当作 route component wrapper
{
  path: '/reports',
  component: defineAsyncComponent(() => import('./views/Reports.vue')),
}

Vue Router 官方说明路由懒加载和 async component 是不同功能。Router 会在首次进入路由时加载并缓存组件;路由懒加载本身也不会因为包在 Suspense 中就自动触发 Suspense fallback。路由组件内部的异步组件仍可以单独使用 Suspense。

参考:Vue Router Lazy Loading Routes

九、v-ifv-show 与异步组件

原文使用 v-if 让异步组件在首次需要显示时才加载:

<template>
  <button @click="showPanel = true">显示组件</button>
  <AsyncPanel v-if="showPanel" />
</template>

这通常可以延迟异步 wrapper 的实际渲染和 loader 调用。v-show 则会创建并挂载组件,只是切换 CSS display,因此不能用它来表达“组件完全不需要加载”:

<AsyncPanel v-show="showPanel" />

选择建议:

  • 组件很少显示、初始化成本高:使用 v-if
  • 组件频繁显示/隐藏,且希望保留 DOM:考虑 v-show
  • 组件切换需要保留实例状态:使用 KeepAlive
  • 组件代码体积较大:使用 import() 做异步组件或路由级代码分割。

如果父组件通过 $refs 调用异步子组件的方法,不应写死 setTimeout(..., 400) 等待。应在 v-if 条件改变后使用 nextTick,或者让子组件通过 ready 事件通知;异步组件本身还要考虑 loader 尚未完成的情况。

import { nextTick } from 'vue'

async function showAndFocus() {
  showPanel.value = true
  await nextTick()
  // 此时只代表同步 DOM 更新完成;若 AsyncPanel 仍在加载,还要等待它自己的 ready 事件
}

十、总结

  • 动态组件通过 <component :is="..."> 决定显示哪个组件。
  • 默认切换动态组件可能卸载旧实例;KeepAlive 才负责缓存实例状态。
  • Vue 3.5 使用 defineAsyncComponent 创建组件级异步 wrapper,可配置 loading、error、delay、timeout、suspensible 和 onError。
  • Suspense 可以提供异步依赖的 fallback,但 Vue 3.5.39 仍将其标为 Experimental,并不自动处理所有错误。
  • Vue 2.7 的 loading/error 高级 factory、Vue 3 的 loadingComponent/errorComponent 不能混写。
  • 路由懒加载使用 component: () => import(...),不要额外包 defineAsyncComponent
  • v-if 可以延迟组件渲染,v-show 只是隐藏已经渲染的组件;KeepAlive 的缓存、异步组件的代码加载和路由 chunk 是三个不同概念。

官方参考

原文出处

作者:职场007

来源:稀土掘金

本文保留原文的动态组件、KeepAlive 和异步组件示例,并补充 Vue 2.7.16、Vue 3.5.39 与 Vue Router 4 的版本边界。

457 DOCUMENTS · 10 COLLECTIONS
ARCHIVE SEARCH457 篇文章

SEARCH GUIDE

输入关键词开始搜索

支持搜索文章标题、所属分类和原始文档路径。

按分类浏览

10 COLLECTIONS