关于 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 不会在服务端执行;涉及 window、document 的逻辑应放在客户端生命周期或客户端专用代码中。
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 回调、分类状态及版权抓取内容已清理。