Vue 中父子组件生命周期执行顺序回顾
原文以 Vue 2 为主,结论是“父组件先创建,子组件先挂载”。这个主线在 Vue 3 的普通同步组件树中仍然成立,但生命周期名称、异步请求和 KeepAlive 的说明需要补充边界。
一、问题背景
在单一组件中,常见的生命周期顺序是:
beforeCreate -> created -> beforeMount -> mounted
-> beforeUpdate -> updated
-> beforeUnmount/unmounted(Vue 3)
-> beforeDestroy/destroyed(Vue 2)
当父组件渲染子组件时,父子组件各自拥有独立的生命周期。渲染器需要先创建父组件上下文,再创建子组件;挂载 DOM 时则通常先完成子组件,再认为父组件的整个子树完成挂载。
以下顺序指的是普通的、同步的、首次渲染的父子组件树。如果组件被 v-if、异步组件、Suspense、KeepAlive 或条件分支包裹,实际执行的钩子会受到这些机制影响。
二、首次加载和挂载顺序
Vue 2 / Vue 3 Options API 的常见顺序
父 beforeCreate
-> 父 created
-> 父 beforeMount
-> 子 beforeCreate
-> 子 created
-> 子 beforeMount
-> 子 mounted
-> 父 mounted
也可以简写为:
父创建 -> 子创建 -> 子挂载 -> 父挂载
父组件的 mounted 通常要等它的同步子组件也挂载完成后才执行,所以原文“子组件先挂载,父组件后挂载”的观察是有依据的。
Vue 3 Composition API 的对应观察
Composition API 没有 beforeCreate 和 created 两个同名 hook;setup() 承担了组件创建阶段的组合式逻辑。常见日志关系可以理解为:
父 setup
-> 父 onBeforeMount
-> 子 setup
-> 子 onBeforeMount
-> 子 onMounted
-> 父 onMounted
setup() 中不能访问已经挂载的 DOM。需要 DOM 时使用 onMounted,需要等待本轮响应式更新后的 DOM 时使用 nextTick()。
为什么父组件先创建、子组件先挂载?
- 创建父组件实例时,需要先解析父组件的 props、状态和渲染函数;
- 父组件 render 得到子组件 vnode 后,渲染器开始创建子组件实例;
- 子组件必须先完成自己的 render 和 DOM patch,父组件的子树才算完成;
- 因此父组件的 mounted/
onMounted放在子组件挂载之后执行。
这不是“父组件会等待所有网络请求”的意思。生命周期顺序只描述组件渲染阶段,不会等待任意 Promise。
三、更新顺序
当一次响应式更新同时导致父、子组件需要更新时,常见顺序是:
父 beforeUpdate
-> 子 beforeUpdate
-> 子 updated
-> 父 updated
Vue 3 的 Composition API 对应:
父 onBeforeUpdate
-> 子 onBeforeUpdate
-> 子 onUpdated
-> 父 onUpdated
但这不是每次数据变化都完整执行的固定脚本:
- 只有真正需要更新的组件才会运行 update hook;
- Vue 会批量合并同步发生的状态变化;
- 父组件更新不一定意味着每个子组件也更新;
- 一个组件的
updated不保证能说明是哪一个属性发生了变化; - DOM 更新完成后仍然应使用
nextTick处理依赖 DOM 的逻辑。
<script setup>
import { nextTick, ref } from 'vue'
const count = ref(0)
async function increment() {
count.value++
await nextTick()
// 此时当前组件因 count 变化产生的 DOM 更新已经完成
}
</script>
四、卸载和销毁顺序
Vue 2 的名称是 beforeDestroy / destroyed;Vue 3 将它们改为 beforeUnmount / unmounted:
父 beforeUnmount / beforeDestroy
-> 子 beforeUnmount / beforeDestroy
-> 子 unmounted / destroyed
-> 父 unmounted / destroyed
Vue 3 Composition API:
<script setup>
import { onBeforeUnmount, onUnmounted } from 'vue'
onBeforeUnmount(() => {
console.log('组件即将卸载')
})
onUnmounted(() => {
console.log('组件及其子树已经卸载')
})
</script>
在卸载前取消请求、移除全局事件、停止观察器;在卸载后不要再访问已经失效的组件 DOM。
五、KeepAlive 对顺序的影响
被 <KeepAlive> 缓存的组件切换离开时不会立即执行普通卸载流程,而是进入停用状态:
首次进入:created/setup -> mounted/onMounted -> activated/onActivated
切换离开:deactivated/onDeactivated
再次进入:activated/onActivated
真正淘汰或父级卸载:unmounted/onUnmounted
Vue 2 对应 activated、deactivated,Vue 3 Composition API 使用 onActivated、onDeactivated。因此不能因为路由离开就假设组件已经 destroyed/unmounted。
六、父组件请求数据,子组件什么时候能拿到?
原文中“父组件请求完成后,子组件在 created 或 mounted 中就一定能拿到接口数据”的说法是不准确的。
例如父组件在 created 或 onMounted 中发起异步请求:
父组件创建/挂载
-> 发起 fetch
-> 子组件按照当前 render 条件创建并挂载
-> fetch 未来某个时间完成
-> 父状态更新
-> 子组件收到新的 prop 并更新
请求不会阻塞 Vue 继续创建子组件。子组件第一次创建时,prop 可能是 undefined、空数组或默认值;请求返回后,父组件更新状态,子组件再收到新的 prop。
方案一:使用 loaded 标志决定是否创建子组件
如果子组件必须在创建时就拿到完整数据,可以让 v-if 控制组件是否创建:
<template>
<div class="test">
<ChildPanel
v-if="loaded"
:data="data1"
/>
<p v-else>加载中……</p>
</div>
</template>
<script setup>
import { ref } from 'vue'
import ChildPanel from './ChildPanel.vue'
const loaded = ref(false)
const data1 = ref(null)
async function loadData() {
data1.value = await fetch('/api/data').then(response => response.json())
loaded.value = true
}
loadData()
</script>
不要简单用 v-if="data1" 代替 loaded,除非你能确定空对象、0、空字符串等都不可能是合法结果。更稳妥的是显式维护 loaded、loading 和 error 状态。
这种方式的特点是:请求完成前子组件根本不存在,所以不会执行子组件的 created/mounted;请求完成后才会按正常顺序创建子组件。
方案二:子组件接受初始值,并 watch prop 变化
如果子组件可以先显示 loading 或空状态,不需要延迟创建它,就让 prop 支持初始值,并监听变化:
Vue 2 Options API
export default {
props: {
data1: {
type: Object,
default: null,
},
},
watch: {
data1: {
immediate: true,
deep: true,
handler(newValue) {
if (newValue) {
this.showData(newValue)
}
},
},
},
methods: {
showData(value) {
// 使用 value 更新子组件自己的 data
},
},
}
Vue 3 Composition API
<script setup>
import { watch } from 'vue'
const props = defineProps({
data1: {
type: Object,
default: null,
},
})
watch(
() => props.data1,
value => {
if (value) {
showData(value)
}
},
{ immediate: true },
)
function showData(value) {
// 使用 value 更新自己的状态
}
</script>
如果只需要显示数据,通常不需要复制到子组件的 data,直接使用 prop 或 computed 即可:
computed: {
displayUrl() {
return this.data1?.url || ''
},
}
子组件不能写 this.data1 = newVal 或 props.data1 = newVal,因为 prop 是父组件传入的只读数据。需要修改时,应 emit 事件通知父组件,或者把 prop 作为初始值复制到一个明确的本地状态中。
方案三:用 computed 派生展示数据
<script setup>
import { computed } from 'vue'
const props = defineProps({
data1: Object,
})
const displayUrl = computed(() => props.data1?.url ?? '')
</script>
<template>
<a v-if="displayUrl" :href="displayUrl">查看详情</a>
<span v-else>暂无链接</span>
</template>
watch 适合执行副作用,例如重新请求、初始化第三方插件;computed 适合从 prop 推导出模板需要的值。不要为了“响应式”而在 watch 中把 prop 再赋值给自己。
七、created 和 mounted 中请求的选择
Vue 2
created:已经可以访问响应式 data、props、methods,但还没有挂载 DOM;适合不依赖 DOM 的数据请求。mounted:组件 DOM 已经挂载;适合必须操作 DOM 的第三方库初始化。
Vue 3
setup()中可以创建状态和请求逻辑;onMounted只用于依赖浏览器 DOM 的逻辑,不要因为“请求必须 mounted”而滥用;- SSR 应考虑
onServerPrefetch、框架的数据获取机制或在服务端路由中加载数据; - fetch 是异步的,放在 created、setup 或 mounted 都不会改变父子组件本身的同步挂载顺序。
八、生命周期中不要使用箭头函数绑定 Options API 的 this
Vue 2/3 Options API 会把生命周期函数的 this 绑定到组件实例,因此不要这样写:
export default {
// ❌ 箭头函数没有自己的 this
created: () => {
this.fetchTodos()
},
}
应使用普通函数:
export default {
created() {
this.fetchTodos()
},
}
Composition API 不依赖组件实例 this,应直接使用闭包中的 refs、props 和 composables:
<script setup>
import { onMounted, ref } from 'vue'
const todos = ref([])
onMounted(async () => {
todos.value = await fetch('/api/todos').then(response => response.json())
})
</script>
九、常见生命周期顺序速查
首次渲染
父 beforeCreate/created/beforeMount
-> 子 beforeCreate/created/beforeMount
-> 子 mounted
-> 父 mounted
更新
父 beforeUpdate
-> 子 beforeUpdate
-> 子 updated
-> 父 updated
卸载
父 beforeUnmount/beforeDestroy
-> 子 beforeUnmount/beforeDestroy
-> 子 unmounted/destroyed
-> 父 unmounted/destroyed
这些顺序是理解普通同步组件树的模型,不应覆盖真实渲染器的所有边界。异步组件、Suspense、Teleport、Transition、KeepAlive、条件分支和批量更新都可能改变观察结果。
十、总结
- 普通首次渲染通常是父组件先创建,子组件先挂载,父组件最后 mounted。
- 父子生命周期不会等待接口请求返回;请求完成后是一次新的响应式更新。
- 子组件需要完整数据时,可以用
loaded配合v-if延迟创建;也可以允许初始空值,再用 watch/computed 响应 prop 变化。 - 子组件不能直接修改 prop,原文中在 watch 里给
this.data1赋值的写法应删除。 - Vue 2 使用
beforeDestroy/destroyed,Vue 3 使用beforeUnmount/unmounted。 - KeepAlive 下离开组件通常是
deactivated,不是销毁;再次进入执行activated。 - Options API 生命周期不要用箭头函数;Composition API 使用
setup和onXxxhooks,不依赖this。
官方参考
原文出处
作者:希希118
原文链接:https://juejin.cn/post/6844904113914773518
来源:稀土掘金。著作权归作者所有,商业转载请联系作者获得授权,非商业转载请注明出处。