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

显示模式

登录
ARCHIVE DOCUMENTVUE

关于 Vue 组件的引入方式:同步和异步,生命周期执行顺序有什么不同?

所属馆藏
Vue
文件格式
Markdown
原始路径
Vue/67-关于vue组件的引入方式有两种,同步和异步,生命周期执行顺序不同
本文目录7 个章节
  1. 一、同步引入组件
  2. 二、异步组件
  3. 三、父子组件生命周期的基本顺序
  4. 四、异步组件的生命周期顺序
  5. 五、生命周期顺序的实践建议
  6. 六、面试版总结
  7. 参考资料

关于 Vue 组件的引入方式:同步和异步,生命周期执行顺序有什么不同?

原文讨论了普通静态导入、异步组件和父子组件生命周期顺序。这个结论需要加上 Vue 版本、是否使用 <Suspense>、组件是否已缓存等前提,不能把“同步/异步”简单理解成两套固定生命周期顺序。

一、同步引入组件

Vue 2.7

import Page from '@/components/Page.vue'

export default {
  components: {
    Page,
  },
}

import 会在模块加载阶段获取组件模块,通常会和当前 JavaScript chunk 一起加载。组件在模板中使用时,Vue 可以直接创建它。

Vue 3.5

普通 SFC 也可以直接静态导入:

<script setup>
import Page from './Page.vue'
</script>

<template>
  <Page />
</template>

静态导入不等于组件一定在首屏真实 DOM 中出现:v-if、动态组件和路由仍然可能让它延后渲染。这里的“同步”主要指模块引用已经可用,而不是保证所有生命周期在当前调用栈同步完成。

二、异步组件

1. Vue 2.7

Vue 2 支持异步组件工厂:

const Page = () => import('@/components/Page.vue')

export default {
  components: {
    Page,
  },
}

import() 会生成独立的代码分片,组件第一次被渲染时才加载对应模块。Vue 2 还常见下面的 Webpack 写法:

const Page = resolve => require([
  '@/components/Page.vue',
], resolve)

原文的 require(['@/components/page'], page)page 没有定义,回调应当使用 resolve,或者直接使用更推荐的 import()

也可以配置加载、错误、超时和延迟:

const Page = () => ({
  component: import('@/components/Page.vue'),
  loading: LoadingComponent,
  error: ErrorComponent,
  delay: 200,
  timeout: 3000,
})

这属于 Vue 2 的异步组件配置形式。

2. Vue 3.5

Vue 3 推荐使用 defineAsyncComponent()

<script setup lang="ts">
import { defineAsyncComponent } from 'vue'

const Page = defineAsyncComponent(() => import('./Page.vue'))
</script>

<template>
  <Page />
</template>

可以配置加载组件、错误组件和超时:

import { defineAsyncComponent } from 'vue'
import LoadingPage from './LoadingPage.vue'
import ErrorPage from './ErrorPage.vue'

const Page = defineAsyncComponent({
  loader: () => import('./Page.vue'),
  loadingComponent: LoadingPage,
  errorComponent: ErrorPage,
  delay: 200,
  timeout: 10_000,
  suspensible: true,
})

defineAsyncComponent 返回的是一个异步组件包装器。loader 返回 Promise,真正加载通常发生在该包装器需要渲染时。路由懒加载也通常使用动态 import()

const routes = [
  {
    path: '/settings',
    component: () => import('./views/SettingsView.vue'),
  },
]

3. 异步组件和代码分割不是一回事

动态 import() 常用于代码分割,异步组件负责在运行时处理加载状态和组件渲染。两者经常一起使用,但概念不同:

  • 代码分割解决 JavaScript 资源何时下载;
  • 异步组件解决组件何时解析、显示加载状态和错误;
  • SSR/Nuxt 还要考虑服务端预渲染、客户端 hydration 和路由预取。

三、父子组件生命周期的基本顺序

以首次挂载、子组件已经可同步创建、没有 KeepAlive/Transition/Suspense 特殊影响为前提,Vue 2 和 Vue 3 的总体规律都是:

父 beforeCreate / setup
父 created(Options API)
父 beforeMount
  子 beforeCreate / setup
  子 created(Options API)
  子 beforeMount
  子 mounted
父 mounted

Vue 3 的 setup() 执行在组件创建早期。它不是一个可以和 Vue 2 created 完全一一对应的生命周期钩子,但在 Options API 组件中通常可以把它理解为“创建阶段的组合式入口”。setup() 中不能访问组件实例的 this

对于一个简单的同步子组件,可以简写为:

父 beforeCreate → 父 created → 父 beforeMount
→ 子 beforeCreate → 子 created → 子 beforeMount
→ 子 mounted → 父 mounted

原因是:父组件先开始挂载并渲染子 VNode;子组件完成挂载后,父组件的挂载流程才完成,所以 mounted 通常表现为“子先、父后”。更新阶段也常见“父 beforeUpdate → 子 beforeUpdate → 子 updated → 父 updated”,但具体顺序还会受到异步调度、组件是否实际更新和 watcher flush 时机影响。

四、异步组件的生命周期顺序

异步组件的 loader 尚未完成时,父组件可能先挂载一个异步组件占位或加载状态。因此在常见的非 Suspense 场景中,可以观察到:

父 beforeCreate / created / beforeMount / mounted
→ 异步组件加载完成
→ 子 beforeCreate / created / beforeMount / mounted

这正是原文想表达的现象,但它不是无条件定律。以下因素会改变观察结果:

  • 异步组件模块已经在缓存中,可能几乎立即完成;
  • Vue 3 使用 <Suspense> 时,父级的挂载提交可能等待异步依赖;
  • 异步组件配置了 loading/error 组件;
  • 组件位于 v-if、动态组件或路由分支中;
  • SSR、hydration、KeepAlive 和 Transition 有额外的生命周期路径。

Vue 3 <Suspense> 示例:

<template>
  <Suspense>
    <template #default>
      <AsyncPage />
    </template>

    <template #fallback>
      <p>加载中……</p>
    </template>
  </Suspense>
</template>

当异步依赖受 Suspense 管理时,默认的 suspensible 行为会影响何时把内容提交到 DOM。不要仅凭“异步组件”三个字断言父 mounted 一定早于子 mounted。

五、生命周期顺序的实践建议

1. 不要依赖父子 mounted 顺序做跨组件通信

如果父组件必须等待子组件完成初始化,优先使用:

  • props/emits;
  • ref 暴露的明确方法;
  • onMounted 后的 nextTick
  • 异步初始化 Promise;
  • 路由数据加载或状态管理。

不要把业务逻辑写成“猜某个 mounted 一定先执行”。

2. DOM 操作放在 mounted 或之后

<script setup lang="ts">
import { nextTick, onMounted, ref } from 'vue'

const input = ref<HTMLInputElement | null>(null)

onMounted(async () => {
  await nextTick()
  input.value?.focus()
})
</script>

<template>
  <input ref="input" />
</template>

服务端渲染时没有浏览器 DOM,onMounted 不会在服务端执行;涉及 windowdocument 的逻辑应放在客户端生命周期或客户端专用代码中。

3. KeepAlive 不是重新 mounted

<KeepAlive> 缓存的组件切换回来时通常触发 activated,不会再次触发 mounted。切换出去时触发 deactivated,只有被淘汰或真正卸载时才会执行 unmounted/destroyed。

4. 异步加载要处理错误和取消

网络失败、超时和用户快速切换都需要有处理策略。请求型逻辑还要避免旧请求覆盖新结果,可使用 AbortController 或 watcher cleanup。

六、面试版总结

静态导入的组件模块通常随当前代码加载,Vue 2 可以使用异步组件工厂,Vue 3 推荐 defineAsyncComponent(() => import(...)),路由懒加载也常使用动态 import。首次同步挂载时,父组件先经历创建和 beforeMount,子组件完成 mounted 后父组件 mounted;异步组件未加载完成时,父组件可能先挂载,子组件在 loader 完成后再创建和挂载。但这不是绝对顺序,<Suspense>、缓存、Transition、SSR 和组件条件渲染都会影响时机。业务代码不应依赖偶然的生命周期顺序,而应通过 props/emits、明确的异步状态和生命周期 API 协作。

参考资料

原文作者:一个前端。原文关于同步/异步组件和生命周期顺序的主线予以保留,错误的 require 回调、分类状态及版权抓取内容已清理。

457 DOCUMENTS · 10 COLLECTIONS
ARCHIVE SEARCH457 篇文章

SEARCH GUIDE

输入关键词开始搜索

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

按分类浏览

10 COLLECTIONS