Vue3父子组件生命周期执行顺序是什么?附setup、onMounted等钩子实战演示
你有没有刚学Vue3就碰到这种情况?明明父组件里写了请求接口的代码,想把拿到的数据传给子组件,结果子组件的页面上一片空白,打开控制台一看数据undefined——大概率就是没搞懂父子组件生命周期的执行先后,接口还没返回就渲染子组件了,今天咱们就彻底捋顺这个问题,不管你用组合式还是选项式API,看完这篇都能直接用。
先把Vue3的核心生命周期钩子分清楚
不管是父子还是单个组件,首先得把Vue3本身支持的两类钩子搞明白:一类是大家可能从Vue2过渡来的选项式API(Options API)钩子,另一类是现在Vue3主推的组合式API(Composition API)钩子,得先对应上两类钩子的功能和触发顺序关系,不然会乱。
选项式的钩子大家应该熟一点,比如beforeCreate、created、beforeMount、mounted这些,组合式的话,setup相当于beforeCreate+created的结合体,但是执行时机稍微有点不一样——Vue3官方说setup在beforeCreate钩子之前运行,实例里的data、methods还没初始化,所以不能在setup里用this,其他组合式的钩子都是在选项式钩子的同一时间点触发,只是写法不同,比如组合式的onBeforeMount对应选项式的beforeMount,onMounted对应mounted,后面我们就把功能一致的放在一起说顺序,方便对比。
单个Vue3组件的生命周期触发顺序(基础)
讲父子之前必须先过一遍单个组件的,不然顺序就串不起来,不管用组合式还是选项式,单个组件的完整加载、更新、卸载流程是固定的,我们只说最常用的加载和卸载流程,更新的话后面单独提一句父子场景。
加载流程(最关键!占父子问题的90%以上)
按时间先后排:
- 组件的setup函数(组合式专属,但不管用不用组合式,只要是Vue3组件,setup都会先于beforeCreate跑,选项式组件如果没写setup,这步就跳过)
- 选项式的beforeCreate钩子
- 选项式的created钩子
- 组合式的onBeforeMount / 选项式的beforeMount(注意是同一时间点,不是先后)
- 组合式的onMounted / 选项式的mounted(同上)
简单验证单个组件的钩子顺序
你可以新建一个Vue3项目,在App.vue里写个空的子组件占位,然后把这段代码放到App.vue的script setup里:
import { onBeforeMount, onMounted, onBeforeUnmount, onUnmounted } from 'vue'
console.log('App setup开始')
// 模拟setup里的逻辑
const test = 'test'
console.log('App setup结束')
onBeforeMount(() => {
console.log('App onBeforeMount')
})
onMounted(() => {
console.log('App onMounted')
})
onBeforeUnmount(() => {
console.log('App onBeforeUnmount')
})
onUnmounted(() => {
console.log('App onUnmounted')
})
然后再写点选项式的代码对比,把setup拆成script和script setup(如果用Vite创建的项目可能需要设置允许双script),控制台打印的结果肯定是先setup的两条,再onBeforeMount,最后onMounted。
进入正题:Vue3父子组件加载、更新、卸载的完整顺序
终于到大家最关心的地方了!父子组件的生命周期,核心原则记住一句话就行:先父组件初始化到挂载前,再子组件从初始化到完全挂载,最后父组件完全挂载——更新和卸载也有类似的“父先启动、子先完成、父再收尾”的逻辑,下面分开细讲。
父子组件首次加载(从无到有渲染出来)
这个是最容易踩数据传递坑的场景,刚才开头说的接口返回晚子组件拿不到数据,就是踩在这里,我们假设父组件叫Parent,子组件叫Child,Child在Parent的template里直接用,没有v-if/v-for条件渲染(条件渲染的情况后面单独加个小补充)。
按时间先后的完整顺序(组合式和选项式同功能钩子放同一层,前面加Parent/Child区分):
- Parent setup开始 → Parent setup结束(组合式专属,没写就跳过)
- Parent beforeCreate(选项式)
- Parent created(选项式)
- Parent onBeforeMount / Parent beforeMount
- Child setup开始 → Child setup结束
- Child beforeCreate(选项式)
- Child created(选项式)
- Child onBeforeMount / Child beforeMount
- Child onMounted / Child mounted
- Parent onMounted / Parent mounted
哦对了!这里一定要注意第10步——只有所有子组件都完全挂载到DOM上之后,父组件的onMounted/mounted才会触发!这就解释了开头的问题:如果你在Parent的setup或者created里发异步请求接口(比如axios.get),接口返回是异步的,不会等数据回来再往下走生命周期,这时候Child已经开始初始化甚至挂载完了,拿到的就是初始的空数据或者undefined。
那怎么解决这个问题呢?简单提两个常用方法,不算跑题,都是围绕生命周期的:
- 第一个是给Child加v-if,父组件拿到数据之后再把v-if设为true,这样Child会等数据回来再重新走一遍从setup到mounted的流程;
- 第二个是在Child里用watch或者watchEffect监听父组件传过来的props,数据变了就重新处理。
父子组件首次加载带条件渲染(v-if/v-for)
刚才的补充里提到了v-if,这里单独说下顺序的变化:假设Parent里有个v-if="showChild",初始showChild是false,Parent首次加载的时候showChild还没改,那Parent的加载流程就会到第4步(Parent onBeforeMount/beforeMount)之后,直接跳第10步(Parent onMounted/mounted),完全不会触发Child的任何钩子;等后来通过点击按钮或者接口返回把showChild改成true,才会单独触发Child从第5步到第9步的加载流程,这时候Parent不会再重新走钩子。
v-for的话,如果是动态数组(初始为空,后来push数据),每push一个Child对应的项,就会单独触发一个新的Child的加载流程,和上面v-if的单个触发逻辑一样。
父子组件的更新流程
更新流程也遵循“父先启动,子先完成,父再收尾”的原则,这里说的更新是指props、data、computed等响应式数据变化导致的DOM更新,同样假设Parent有个响应式数据count,传给了Child当props:
- Parent的响应式数据count变了
- Parent onBeforeUpdate / Parent beforeUpdate
- Child的响应式props count变了
- Child onBeforeUpdate / Child beforeUpdate
- Child的DOM更新完成
- Child onUpdated / Child updated
- Parent的DOM更新完成
- Parent onUpdated / Parent updated
父子组件的卸载流程
卸载流程和加载流程是完全反过来的!也是“父先启动,子先完成,父再收尾”:
- Parent onBeforeUnmount / Parent beforeUnmount
- Child onBeforeUnmount / Child beforeUnmount
- Child的组件实例和DOM被销毁
- Child onUnmounted / Child unmounted
- Parent的组件实例和DOM被销毁
- Parent onUnmounted / Parent unmounted
用一个完整的实战案例验证所有加载顺序
光说理论可能记不住,咱们写一个同时包含组合式和选项式钩子的Parent+Child案例,直接看控制台打印结果。
先写组合式的父组件(Parent.vue)
<template>
<div class="parent-box">
<h2>我是父组件</h2>
<button @click="count++">增加父组件count</button>
<button @click="showChild = !showChild">切换子组件显示</button>
<Child v-if="showChild" :parentCount="count" />
</div>
</template>
<script setup>
import { ref, onBeforeMount, onMounted, onBeforeUpdate, onUpdated, onBeforeUnmount, onUnmounted } from 'vue'
import Child from './Child.vue'
console.log('Parent setup开始')
const count = ref(0)
const showChild = ref(true)
console.log('Parent setup结束,count初始值:', count.value)
onBeforeMount(() => {
console.log('Parent onBeforeMount')
})
onMounted(() => {
console.log('Parent onMounted')
})
onBeforeUpdate(() => {
console.log('Parent onBeforeUpdate')
})
onUpdated(() => {
console.log('Parent onUpdated')
})
onBeforeUnmount(() => {
console.log('Parent onBeforeUnmount')
})
onUnmounted(() => {
console.log('Parent onUnmounted')
})
</script>
<style scoped>
.parent-box {
border: 2px solid #42b983;
padding: 20px;
margin: 20px;
}
</style>
再写选项式+组合式混合的子组件(Child.vue)
故意混合写是为了验证同功能钩子的触发时间是否一致。
<template>
<div class="child-box">
<h3>我是子组件</h3>
<p>父组件传过来的count:{{ parentCount }}</p>
</div>
</template>
<script>
export default {
props: ['parentCount'],
beforeCreate() {
console.log('Child beforeCreate(选项式)')
},
created() {
console.log('Child created(选项式)')
},
beforeMount() {
console.log('Child beforeMount(选项式)')
},
mounted() {
console.log('Child mounted(选项式)')
},
beforeUpdate() {
console.log('Child beforeUpdate(选项式)')
},
updated() {
console.log('Child updated(选项式)')
},
beforeUnmount() {
console.log('Child beforeUnmount(选项式)')
},
unmounted() {
console.log('Child unmounted(选项式)')
}
}
</script>
<script setup>
import { onBeforeMount, onMounted, onBeforeUpdate, onUpdated, onBeforeUnmount, onUnmounted } from 'vue'
console.log('Child setup开始')
console.log('Child setup结束,parentCount初始值:', props.parentCount) // 这里props不用显式defineProps吗?哦对,混合写的时候script里的props会自动暴露给script setup,不过建议还是显式写,这里为了验证混合才这么写
onBeforeMount(() => {
console.log('Child onBeforeMount(组合式)')
})
onMounted(() => {
console.log('Child onMounted(组合式)')
})
onBeforeUpdate(() => {
console.log('Child onBeforeUpdate(组合式)')
})
onUpdated(() => {
console.log('Child onUpdated(组合式)')
})
onBeforeUnmount(() => {
console.log('Child onBeforeUnmount(组合式)')
})
onUnmounted(() => {
console.log('Child onUnmounted(组合式)')
})
</script>
<style scoped>
.child-box {
border: 2px solid #35495e;
padding: 20px;
margin-top: 20px;
}
</style>
看控制台的打印结果
把Parent.vue引入到App.vue里,运行项目:
- 首次加载(showChild初始为true): Parent setup开始 → Parent setup结束 → Parent beforeCreate(这里Parent没写选项式的beforeCreate和created,所以没有)→ Parent onBeforeMount → Child setup开始 → Child setup结束 → Child beforeCreate(选项式)→ Child created(选项式)→ Child beforeMount(选项式)和Child onBeforeMount(组合式)(顺序可能随机?但几乎同时)→ Child mounted(选项式)和Child onMounted(组合式)(同上)→ Parent onMounted
- 点击“增加父组件count”: Parent onBeforeUpdate → Child beforeUpdate(选项式)和Child onBeforeUpdate(组合式)→ Child updated(选项式)和Child onUpdated(组合式)→ Parent onUpdated
- 点击“切换子组件显示”隐藏Child: Parent onBeforeUpdate → Child beforeUnmount(选项式)和Child onBeforeUnmount(组合式)→ Child unmounted(选项式)和Child onUnmounted(组合式)→ Parent onUpdated
- 再次点击“切换子组件显示”显示Child: Parent onBeforeUpdate → Child setup开始 → ... → Child mounted → Parent onUpdated
- 刷新页面或者直接关闭标签(相当于卸载App.vue,Parent作为App的子组件也会卸载): App的onBeforeUnmount(如果App.vue写了的话)→ Parent onBeforeUnmount → Child beforeUnmount(如果还在显示)→ Child unmounted → Parent onUnmounted → App的onUnmounted
总结一些实用的小技巧
- 发异步接口请求全局/父组件数据:尽量放在Parent的setup或者created里,但别等返回再渲染,可以用v-if控制子组件,或者子组件监听props;
- 发异步接口请求子组件专属数据:放在Child的setup或者created里,或者Child的onMounted里(如果需要DOM元素的话);
- 操作子组件的DOM元素:必须放在Parent的onMounted里,因为只有这时候Child才完全挂载;
- 混合写组合式和选项式API:同功能钩子的触发时间几乎一致,没有绝对的先后,尽量只用一种,方便维护。 就到这里,你可以自己把上面的代码复制下来跑一遍,印象会更深刻,如果还有其他Vue3的问题,欢迎在评论区留言!
版权声明
本文仅代表作者观点,不代表Code前端网立场。
本文系作者Code前端网发表,如需转载,请注明页面地址。
code前端网



