今天偶然看到一个问题,是关于vue父子组件生命周期的,自己最后也没有写出正确的答案,所以特此记录一下这个知识点。(以下均为基于vue2.x的情况,vue3.x生命周期有所变化,但是同样可以类比)
问题
首先给出我遇到的问题,这也是平常笔试面试可能遇到的问题:
假设以下生命周期中含有下列功能
- 父组件beforeCreate:打印a1
- 父组件created:打印a2
- 父组件beforeMount:打印a3
- 父组件mounted:打印a4
- 子组件beforeCreate:打印b1
- 子组件created:打印b2
- 子组件beforeMount:打印b3
- 子组件mounted:打印b4
那在执行过程中,控制台打印的结果是什么?
结果
这个问题考点其实就是
父组件和子组件在执行时的生命周期调用顺序
,我们可以思考之后给出自己的答案,然后在代码上加以验证看看是否正确,这里直接给出答案:a1 a2 a3 b1 b2 b3 b4 a4。你的答案正确吗?
思考
从上面的答案不难反推出,当父子组件在构建渲染时,生命周期存在这样的关系:
父beforeCreate->父created->父beforeMount->子beforeCreate->子created->子beforeMount->子mounted->父mounted
接下来就来探究一下为什么会是这样的顺序
生命周期
首先了解一下生命周期,上一张官方生命周期图
先不用去考虑上图中具体的细节,只要了解图中的流程就可以理解今天这个问题。根据官方文档,vue生命周期钩子函数中要完成的事件可以整理为下表:
生命周期钩子函数 | 事件以及处理 |
---|---|
beforeCreate | 在实例初始化之后,数据观测(data observer) 和 event/watcher 事件配置之前被调用。 |
created | 实例已经创建完成之后被调用。在这一步,实例已完成以下的配置:数据观测(data observer),属性和方法的运算, watch/event 事件回调。然而,挂载阶段还没开始,$el 属性目前不可见。 |
beforeMount | 在挂载开始之前被调用:相关的 render 函数首次被调用。 |
mounted | el 被新创建的 vm.$el 替换,并挂载到实例上去之后调用该钩子。如果 root 实例挂载了一个文档内元素,当 mounted 被调用时 vm.$el 也在文档内。 |
beforeUpdate | 数据更新时调用,发生在虚拟 DOM 重新渲染和打补丁之前。你可以在这个钩子中进一步地更改状态,这不会触发附加的重渲染过程。 |
updated | 由于数据更改导致的虚拟 DOM 重新渲染和打补丁,在这之后会调用该钩子。当这个钩子被调用时,组件 DOM 已经更新,所以你现在可以执行依赖于 DOM 的操作。 |
activated | keep-alive 组件激活时调用。 |
deactivated | keep-alive 组件停用时调用。 |
beforeDestroy | 实例销毁之前调用。在这一步,实例仍然完全可用。 |
destroyed | Vue 实例销毁后调用。调用后,Vue 实例指示的所有东西都会解绑定,所有的事件监听器会被移除,所有的子实例也会被销毁 |
上面的介绍,多少有点抽象,接下来就结合实例来进一步分析这个过程
实例测试
这里给出一段调试代码:
var vm = new Vue({
el: '#app',
data: {
message: 'Vue的生命周期'
},
beforeCreate: function() {
console.group('------beforeCreate创建前状态------');
console.log("%c%s", "color:red" , "el : " + this.$el); //undefined
console.log("%c%s", "color:red","data : " + this.$data); //undefined
console.log("%c%s", "color:red","message: " + this.message)
},
created: function() {
console.group('------created创建完毕状态------');
console.log("%c%s", "color:red","el : " + this.$el); //undefined
console.log("%c%s", "color:red","data : " + this.$data); //已被初始化
console.log("%c%s", "color:red","message: " + this.message); //已被初始化
},
beforeMount: function() {
console.group('------beforeMount挂载前状态------');
console.log("%c%s", "color:red","el : " + (this.$el)); //已被初始化
console.log(this.$el);
console.log("%c%s", "color:red","data : " + this.$data); //已被初始化
console.log("%c%s", "color:red","message: " + this.message); //已被初始化
},
mounted: function() {
console.group('------mounted 挂载结束状态------');
console.log("%c%s", "color:red","el : " + this.$el); //已被初始化
console.log(this.$el);
console.log("%c%s", "color:red","data : " + this.$data); //已被初始化
console.log("%c%s", "color:red","message: " + this.message); //已被初始化
},
beforeUpdate: function () {
console.group('beforeUpdate 更新前状态===============》');
console.log("%c%s", "color:red","el : " + this.$el);
console.log(this.$el);
console.log("%c%s", "color:red","data : " + this.$data);
console.log("%c%s", "color:red","message: " + this.message);
},
updated: function () {
console.group('updated 更新完成状态===============》');
console.log("%c%s", "color:red","el : " + this.$el);
console.log(this.$el);
console.log("%c%s", "color:red","data : " + this.$data);
console.log("%c%s", "color:red","message: " + this.message);
},
beforeDestroy: function () {
console.group('beforeDestroy 销毁前状态===============》');
console.log("%c%s", "color:red","el : " + this.$el);
console.log(this.$el);
console.log("%c%s", "color:red","data : " + this.$data);
console.log("%c%s", "color:red","message: " + this.message);
},
destroyed: function () {
console.group('destroyed 销毁完成状态===============》');
console.log("%c%s", "color:red","el : " + this.$el);
console.log(this.$el);
console.log("%c%s", "color:red","data : " + this.$data);
console.log("%c%s", "color:red","message: " + this.message)
}
})
运行后控制台得到的结果为:
从以上实例可以看出,在组件的beforeCreate阶段,并不能监测到数据,data为undefined,$el也是undefined,
无可操作数据
。实际上这个时候只进行了一些必要的初始化操作,比如全局配置、属性初始化等
在created阶段,
组件实例已经完成了数据代理和数据的绑定
,data已经有值(这个时候其实methods中的方法也已经完成加载,已经watch/event回调),但是$el属性依然是不可见的。
在beforeMount阶段,结合生命周期图,我们可以知道调用这个钩子函数前会
首先执行模板解析函数
,这个阶段$el属性已经可以打印出值,但是从打印结果可以看出(如下)
el属性中的message
并未被替换成动态数据
。其实这里使用的就是虚拟Dom,先把坑占下了,等待元素
真正挂载到页面时
再把值渲染进去。
在mounted阶段,可以看到值已经被渲染,el也被挂载到了实例上(如图)
结论
所以结合上面这个实例,可以得出,当父组件进行到beforeMount阶段,即开始模板解析时,就发现需要去构建子组件,所以这个时候会触发子组件的生命周期,当子组件执行到mounted阶段时,子组件已经挂载到了父组件上,父组件随之进入到自身的mounted阶段,将自身挂载。
延伸
除了渲染过程,还有父子组件更新、销毁等过程,都可以按照上面的方式来自己推敲(实例中也写了相关的生命周期钩子),这里直接给出结论
- 子组件更新:父beforeUpdate->子beforeUpdate->子updated->父updated
- 父组件更新:父beforeUpdate->父updated
- 父组件销毁:父beforeDestroy->子beforeDestroy->子destroyed->父destroyed
参考资料