Vue生命周期小结

0.179字数 1978阅读 249

Vue框架已日臻成熟,生命周期也算是老生常谈了。网路上也有很多对Vue生命周期的讲解。
此处是补充上自己的理解,再次总结一下。

一、什么是生命周期(LifeCycle)?

生命周期在计算机语言里,生命周期一般是指一个对象的创建(生)到销毁(死)的阶段。

二、Vue的生命周期

2.1 生命周期图解

对于Vue的生命周期,就是其组件的生命周期。具体可以看下图(相对官方文档的图,已补充翻译):

vue-lifecycle(该翻译图忘记来源,有知道绘制作者提供下).png

从图中可以直观注意到,Vue的生命周期可以划分为四个阶段:

  • create 阶段: vue实例被创建;
  • mount 阶段: vue实例被挂载到真是的DOM节点;
  • update 阶段:当vue实例里面的data数据变化时,触发组件的重新渲染;
  • destroy 阶段:vue实例被销毁。

2.2 阶段分解

上面的四个阶段,每个阶段分为开始前和开始后,这样就衍生出了9个方法:

  • beforeCreate
  • created
  • beforeMount
  • mounted
  • beforeUpdate
  • updated
  • beforeDestroy
  • destroyed
    ——也就是所谓的钩子函数

接下来我们一个阶段一个阶段来分析它们的触发条件及表征。

2.2.0 实验代码准备

(1)一个最简单的Vue实例化代码如下:

<div id="app">
    <p>{{ message }}</p>
</div>
var vm = new Vue({
        el: '#app',
        data: {message: 'Hello Vue~'},
});

我们主要关心着这个实例化里的属性:元素el什么时候有值(挂载上了?)、data什么时候有数据?、message又是什么时候有数据呢?
于是,我们在每个回调里面去打印下这三个值看看。

(2)为了更直观去理解Vue实例的变化,我们引用了一段实验代码(该源码来源于参考文章1):

<div id="app">
    <p>{{ message }}</p>
</div>

<script type="text/javascript">
    var vm = new Vue({
        el: '#app',
        data: {message: 'Hello Vue~'},
        beforeCreate: function(){
            console.group('beforeCreate 创建前 =========')
            console.log('%c%s','color:red','el      :',this.$el) // 1 => undefined
            console.log('%c%s','color:red','data    :',this.$data) // 1 => undefined
            console.log('%c%s','color:red','message :',this.message) // 1 => undefined

        },
        created: function(){
            console.group('created 创建完毕 =========')
            console.log('%c%s','color:red','el      :',this.$el) // 1 => undefined
            console.log('%c%s','color:red','data    :',this.$data) // 1 => {message: 'Hello Vue~'}
            console.log('%c%s','color:red','message :',this.message) // 1 => 'Hello Vue~'

        },
        beforeMount: function(){
            console.group('beforeMount 挂载前 =========')
            console.log('%c%s','color:red','el      :',this.$el) // 1 => <div id="app"><p>{{ message }}</p></div>
            console.log('%c%s','color:red','data    :',this.$data) // 1 => {message: 'Hello Vue~'}
            console.log('%c%s','color:red','message :',this.message) // 1 => 'Hello Vue~'

        },
        mounted: function(){
            console.group('mounted 挂载结束 =========')
            console.log('%c%s','color:red','el      :',this.$el) // 1 => <div id="app"><p>'Hello Vue~'</p></div>
            console.log('%c%s','color:red','data    :',this.$data) // 1 => {message: 'Hello Vue~'}
            console.log('%c%s','color:red','message :',this.message) // 1 => 'Hello Vue~'

        },
        beforeUpdate: function(){
            console.group('beforeUpdate 更新前 =========')
            console.log('%c%s','color:red','el      :',this.$el) // 2 => <div id="app"><p>'Celine~'</p></div>
            console.log('%c%s','color:red','data    :',this.$data) // 2 => {message: 'Celine~'}
            console.log('%c%s','color:red','message :',this.message) // 2 => 'Celine~'

        },
        updated: function() {
            console.group('updated 更新完成 =========')
            console.log('%c%s','color:red','el      :',this.$el) // 2 => <div id="app"><p>'Celine~'</p></div>
            console.log('%c%s','color:red','data    :',this.$data) // 2 => {message: 'Celine~'}
            console.log('%c%s','color:red','message :',this.message) // 2 => 'Celine~
            
        },
        beforeDestroy: function(){
            console.group('beforeDestroy 销毁前 =========')
            console.log('%c%s','color:red','el      :',this.$el) // 3 => <div id="app"><p>'Celine~'</p></div>
            console.log('%c%s','color:red','data    :',this.$data) // 3 => {message: 'Celine~'}
            console.log('%c%s','color:red','message :',this.message) // 3 => 'Celine~

        },
        destroyed: function(){
            console.group('destroyed 销毁完成 =========')
            console.log('%c%s','color:red','el      :',this.$el) // 3 => <div id="app"><p>'Celine~'</p></div>
            console.log('%c%s','color:red','data    :',this.$data) // 3 => {message: 'Celine~'}
            console.log('%c%s','color:red','message :',this.message) // 3 => 'Celine~

        }
    })

    // 1 页面直接刷新进来,执行的钩子有 beforeCreate / created / beforeMount / mounted 四个。
    // 2 在控制台执行  vm.message = "Celine~" 后,执行的钩子有 beforeUpdate / updated 两个。
    //   更新前和更新后的,打印数据均是新数据 ?? 这点和想象的不太一致。
    // 3 在控制台执行  vm.$destroy() 后,执行的钩子有 beforeDestroy / destroyed 两个。
    //   销毁前和销毁后,数据依旧存在?? 这点也和想象的不太一致。不过这个时候再去改变message值,vue不会再响应(也不会去执行beforeUpdate / updated 钩子)
    // 4  在控制台执行  vm.message = "Bye~" ,没有任何钩子有响应。

</script>

返回结果:


由此可见,当代码运行时,会一次调用Vue 的 beforeCreate、created、beforeMount、mounted 四个方法,直至完成组件的挂载。
update阶段,要在数据发生改变时(比如更新message字段 vm.message = 'Hahahaha~')才出发;destroy阶段,要在调用vm.$destroy()后才触发。

下面我们来分别分析每个阶段:

2.2.1 create阶段

从生命周期图可以看出,在这个阶段主要做两件事:

  • 监控Data数据
  • 初始化内部事件

从控制台打印数据,可以看出:在beforeCreate时,因为啥动作都还没有开始,所以vm.$el,vm.$data,vm.message都是undefined
而在created时,因为已开始监控Data数据,所以data,message都有值。至于初始化内部事件是什么,我们此处暂不表,后续补充。

2.2.2 mount阶段

在这个阶段,做的事情就比较多了。

挂载前:

  1. 先判断是否有el选项(我们的上方示例代码中有该选项,即el: '#app',);
  2. 如果有el选项,接着判断是否有template选项(此处我们的示例代码中并没有该选项);
  3. 如果有template选项,编译该模板并导入到渲染函数;如果没有template选项,就把实例模板el指向的DOM的outerHTML(上方示例代码即为<div id="app"><p>{{ message }}</p></div>)作为模板.

挂载后:

  1. 创建vm.$el,并用上面编译好的模板(html内容)替换el指向的DOM。

从控制台打印数据就可以看出:
挂载前,vm.$el<div id="app"><p>{{ message }}</p></div>,看起来似乎有值了,但又不太对劲——因为message的值没有代入进入。其实此处有点像虚拟DOM的效果:也就是我的vm.$el虽然不是完整的,但也先准备着。(所以,在其他版本的浏览器中,此处也可能是打印出undefined。)
挂载结束就不一样了,三个数据项都就绪了。vm.$el也成为了完美的<div id="app"><p>Hello Vue~</p></div>

以上的结果是按照有el选项,没有template选项的情况执行的。接下来我们看看其他种情况下,会发生点什么?

(1)没有el选项
操作:注释掉 el: "#app"
结果:只进行了create阶段,没有进行mount阶段。
原因分析:这个好理解,因为没有了el选项,就无从挂载起了。生命周期也就结束了。
如果此时想进行挂载,可以手动去调用vm.$mount(el)

(2)手动挂载
操作:new Vue({...})后面执行 vm.$mount('#app')
结果:进行了create阶段后,也进行了mount阶段。

(3)有template选项时
之前的实例代码是没有template选项的情况表现,此处我们看下若有template选项,会发生点什么呢?

    var vm = new Vue({
        el: '#app',
        data: {message: 'Hello Vue~'},
        template: "<h1>我是模板标题1</h1>", 
        }

操作:new Vue({...})里新增 template选项,如上。
结果: vm.$el变成了template选项的内容;DOM节点#app也替换成template选项的内容了。其实挂载后,vm.$el是什么,DOM节点#app也对应是什么,它们是等价的。
分析: 此处验证了前面生命周期图:若有template选项,就编译它并导入到渲染函数;若没有template选项,就取#app的outerHTML作为模板。

(4)render()方法
Vue实例里还有render()方法可以提供模板,我们看下如果存在render()方法,会发生什么?

    var vm = new Vue({
        el: '#app',
        data: {message: 'Hello Vue~'},
        template: "<h1>我是模板标题1</h1>", 
                render(h){
            return h('h2','这是render出来的标题2');
        },
        }

操作:new Vue({...})里新增 render(h)方法,如上。
结果: vm.$el变成了render返回的模板内容。
分析: 也就是说,渲染模板的优先级可以小结为:render()方法 > template选项 > el属性的outerHTML。

以上就是挂载阶段的一序列可能性变化。接下来我们看下更新阶段。

2.2.3 update阶段


更新阶段的前提是:“when data changes”也就是说当data选项里的数据有变化时触发。
让数据改变有很多操作方式,此处我们简单的在控制台对message字段进行改写。

// 在控制台输入:
vm.message = "Now update!!" //直接回车


当进行了数据更新,就会触发beforeUpdate方法和 updated方法。
此处在控制台打印出来的数据和预想的有出入:beforeUpdate本应该输出的旧数据(message: Hello Vue~),但此处更新前后的数据却显示一样。

在参考文章2里面,说这是打印出来的是虚拟DOM,都已更新,但真实DOM还没有改变。但我个人觉得不一定是这样。我尝试过在各个钩子函数补充打印出DOM元素(如下代码),但结果更新前后也都是更新后的数据。

console.log('%c%s','color:red','#app DOM :',document.getElementById("app"))

我个人更倾向于是因为控制台本身原因。在beforeUpdate时可能确实是旧数据,只不过往下执行updated时候,更新新数据时,也改写了beforeUpdate部分的数据。(待进一步探讨研究补充。

2.2.4 destroy阶段

image.png

销毁阶段,需要执行vm.$destroy()才会进入。
同样的,我们在控制台执行销毁方法,得到如下结果:

可以看出,销毁前后的数据是一样的,但实际上,销毁后Vue实例会接触所有绑定,所有事件被移除,子组件被销毁。比如我们此处更新 data 数据项 vm.message,可以发现,不会在触发update阶段了。

2.3 生命周期小结

我们对上面的分析结果做个小结,此处的表格会多考虑两个方法(当有<keep-alive></keep-alive>组件时,生命周期会多出现一个activate阶段)。

方法名 特征 属性变化
beforeCreate 组件实例创建前(或者说刚被创建),啥也没有。 $el、data 的值都为undefined。
created 组件实例创建完成。属性已绑定,但DOM还未产生。 data有值了,$el属性还是undefined。
beforeMount 模板编译/挂载前。 $el是虚拟DOM。
mounted 模板编译/挂载后。 “虚拟”的dom节点被真实的dom节点替换,并将其插入到dom树中。此时可以获取到$el为真实的dom元素。
beforeUpdate 组件更新之前。 $el、data 的值都为新数据。
updated 组件更新之后。 $el、data 的值都为新数据。
activated for kepp-alive,组件被激活时调用。 (待补充)
deactivated for kepp-alive,组件被移除时调用。 (待补充)
beforeDestroy 组件销毁前嗲用。此时实例仍可用。 $el、data 都有值。实例绑定的事件还存在。
destroyed 组件销毁后调用。 $el、data 虽然都有值。但实例绑定的事件和子组件都没有了。

3 了解生命周期的作用

我们去关注声明周期,是为了能更好的判断在不同的生命周期钩子函数里面做些什么操作和处理。比如:

  • beforeCreate - 加入loading事件
  • created - 结束loading
  • beforemount - 发起服务端请求,取数据
  • mounted - 根据请求数据,对页面DOM做些什么操作
    ……

具体每个阶段的做些什么,还是要根据实际场景来设定咯~

-----------------------HAPPY END--------------------------------


参考文献:

  1. segmentFaul: Vue2.0 探索之路——生命周期和钩子函数的一些理解 本文的主实例代码来源
  2. 简书:05、手把手教Vue--生命周期 本文挂载阶段的分类案例来源
  3. cnblogs:Vue生命周期 声明周期分析方法最初来源

推荐阅读更多精彩内容