通俗理解之Vue -- 生命周期

155 阅读5分钟

一、生命周期概念

首先 生命周期是 一个vue实例从创建到销毁的过程,在这个过程中,他经历了从创建、初始化数据、编译模版、挂载Dom、渲染->更新->渲染、卸载等一系列过程。

1585_1.png 这个过程包括了这几个生命周期函数,常见的分别有: beforeCreate,created, beforeMount,mounted, beforeUpdate,updated, beforeDestory,destoryed 不常见的是keep-alive缓存的组件激活或停用时会触发的actived和deactivated,还有捕获子孙组件报错的errorCaptured

二、详细过程

1、 new Vue() -> beforeCreate()

new Vue()实例化一个vue实例后,会先初始化这个实例的生命周期和事件相关属性。首先我先看看Vue实例的构造函数。

function Vue (options) { 
    this._init(options) 
}

在init()函数进行初始化,并且在初始化过程中分别调用了3个初始化函数initLifecycle(),initEvents(),initRender()

  • initLifecycle(): 初始化生命周期,在这个时候定义了一些属性比如表示当前生命状态的_isMounted ,_isDestroyed ,_isBeingDestroyed,表⽰keep-alive中组件状态的_inactive;以及parent,child等属性。
  • initEvents(): 初始化事件相关属性,定义了once、off、emit、on几个函数。
  • initRender():初始化 render 函数,定义了 createElement 函数。
2、执行beforeCreate()

实例初始化完成后,实例的props解析完成后,调用beforeCreate(),这时候的data,watch,computed,method等选项还未处理,不能访问到里面的方法和数据。一般情况下这个钩子不做业务逻辑,但可以增加混合,或者加个loading事件。

3、 beforeCreate() -> created()

beforeCreate()执行完后,会开始进行数据初始化、在这个过程中会定义data数据、方法、以及事件,并且完成数据劫持observe以及给组件实例配置watcher观察者实例(后续的数据发生变化时,才能感知到数据的变化并且完成页面的渲染)。

4、 created()

执⾏created⽣命周期函数时,已经可以拿到data下的数据以及methods下的⽅法了,所以在这⾥,我们可以开始调⽤⽅法进⾏数据请求了,但此时挂载阶段还没开始,$el属性仍不可用。 一般情况下,数据请求一般放在这里。因为created是在实例创建完毕后立即调用,这时候页面dom节点还未生成,而mounted的时候,dom节点已经生成了,请求到数据后再去修改属性,有可能导致页面闪动。所以数据请求一般放在created里。

5、created() -> beforeMount()

1755_1.png 这里的过程就比较复杂:

  • 首先先判断是否存在el选项,如果不存在则停止编译,直到调用vm.$mount(el)才会继续编译
  • 在判断是否有template模板之前,其实还有一个判断---判断当前是否存在有render函数,如果有的话就直接使用当前的render函数,如果没有我们才开始检查是否有template模板
  • 判断是否有template模板,如果有的话将template模板转换成render函数,如果没有template,那么我们就会直接将获取到的el(也就是我们常见的#app,#app⾥⾯可能还会有其他标签)编译成templae, 然后再将这个template转换成render函数。
关于el选项

el选项提供一个在页面上已存在的 DOM 元素作为 Vue 实例的挂载目标。可以是 CSS 选择器,也可以是一个 HTMLElement 实例。在实例挂载之后,元素可以用 vm.el 访问。如果在实例化时存在这个选项,实例将立即进入编译过程,否则,需要显式调用 vm.el 访问。如果在实例化时存在这个选项,实例将立即进入编译过程,否则,需要显式调用 vm.mount() 手动开启编译。
没有el选项会有怎么样的后果?
如果没有el选项则停止编译,也意味着暂时停止了生命周期,直到手动调用vm.$mount(el)函数时再判断是否指定template。 以下网友展示:

new Vue({
      el"#app",
      beforeCreatefunction () {
        console.log("调用了beforeCreat钩子函数");
      },
      createdfunction () {
        console.log("调用了created钩子函数");
      },
      beforeMountfunction () {
        console.log("调用了beforeMount钩子函数");
      },
      mountedfunction () {
        console.log("调用了mounted钩子函数");
      },
    });

正常情况有el选项打印如下,可以看到el选项有且正确时,生命周期正常执行。

1755_1.png

// 没有el选项
new Vue({
      beforeCreatefunction () {
        console.log("调用了beforeCreat钩子函数");
      },
      createdfunction () {
        console.log("调用了created钩子函数");
      },
      beforeMountfunction () {
        console.log("调用了beforeMount钩子函数");
      },
      mountedfunction () {
        console.log("调用了mounted钩子函数");
      },
    });

当去掉el选项,我们可以看到生命周期钩子执行到created就结束了。

1758_1.png

// 手动调用vm.$mount("#app");
var vm = new Vue({
      beforeCreatefunction () {
        console.log("调用了beforeCreat钩子函数");
      },
      createdfunction () {
        console.log("调用了created钩子函数");
      },
      beforeMountfunction () {
        console.log("调用了beforeMount钩子函数");
      },
      mountedfunction () {
        console.log("调用了mounted钩子函数");
      },
    });
    vm.$mount("#app");

我们可以虽然没有el选项,但是我们通过vm.$mount("#app")手动添加也能顺利执行生命周期。

1757_2.png

关于template

一个字符串模板作为Vue实例的标识使用。如果el存在,模板将会替换挂载的元素。挂载元素的内容都将被忽略,除非模板的内容有分发插槽。如果值以#开始,则它将被用作选择符,并使用匹配元素的 innerHTML作为模板(template: '#temp';会匹配id为temp的元素作为template)。 通俗来讲就是template选项就是一个dom字符串,类似于"<div>我是template的内容:小明今年{{age}}岁了</div>",然后这字符串会取缔挂载元素(<div id='#app'>)内容。

    <head>
        <meta charset="utf-8" />
        <script src="https://cdn.jsdelivr.net/npm/vue@2.6.10/dist/vue.js"></script>
 </head>
    <body>
        <div id="app">
            我是el挂载的内容:小明今年{{age}}岁了
        </div>
    </body>
    <script>
        const vm= new Vue({
            el:'#app',
            data:{
                age:17
            },
            template:'<div>我是template的内容:小明今年{{age}}岁了</div>'
        })
    </script>
    

template里面的模板会替换掉#app里面的内容,显示如下:

1760_1.png
html结构如下:

1761_1.png

关于render函数
  new Vue({ render: h => h(App) })  

作用就是将传入的模板编译成vnode,而代入到vue文件编译为原生html的主要流程如下,render函数就在第三步:

  1. 提取模板中的原生html和非原生html,以及属性,事件,指令等。
  2. 经过一些处理生成render函数。
  3. render函数将模板函数内容生成对应的vnode。
  4. 在经过patch过程diff得到要渲染到视图中的vnode。
  5. vue根据vnode创建真实的dom节点,也就是原生html插入到视图中,完成渲染。
6、beforeMount()
  • beforeMount在组件被挂载之前调用。当这个钩子被调用时,组件已经完成了其响应式状态的设置,但还没有创建 DOM 节点。它即将首次执行 DOM 渲染过程,开始渲染render函数这里一般不会增加业务逻辑
7、beforeMount() -> mounted()
* 先生成一个虚拟dom
* 然后通过diff算法新⽼虚拟dom对⽐计算,通过render函数将其渲染成真实dom,渲染出来的真实dom替换掉原来的vm.el,然后再将替换后的el append到我们的页⾯内。整个初步流程就算是⾛完了
8、mounted()

mounted在组件被挂载之后调用。此时可以通过DOM API获取到DOM节点,$ref属性可以访问。就是数据和dom都完成挂载。并且标识⽣命周期的⼀个属性_isMounted 置为true。所以mounted函数内,我们是可以操作dom的,因为这个时候dom已经渲染完成了。

9、beforeUpdate()
  • beforeUpdate在组件即将因为一个响应式状态变更而更新其 DOM 树之前调用。简单来说就是对页面上的数据有影响的变化(注意是页面上的数据,要应用到视图才会变化),都会触发这个生命周期函数,但是在这里只更新了虚拟dom上的数据,还没更新到视图上。
注意 这里有个判断
  • 判断当前的_isMounted是不是为ture并且_isDestroyed是不是为false,也就是说,保证dom已经被挂载的情况下,且当前组件并未被销毁,才会⾛update流程。
10、updated()

updated⾥⾯也可以操作dom,并拿到最新更新后的dom。不过这⾥我要插⼀句话了,mouted和updated的执⾏,并不会等待所有⼦组件都被挂载完成后再执⾏,所以如果你希望所有视图都更新完毕后再做些什么事情,那么你最好在mouted或者updated中加⼀个nextTick(),然后把要做的事情放在nextTick(),然后把要做的事情放在netTick()中去做。

11、beforeDestroy()
  • 实例销毁之前调用。在这一步,实例仍然完全可用。该钩子在服务器端渲染期间不被调用。
12、destroyed()
  • 实例销毁后调用。该钩子被调用后,对应 Vue 实例的所有指令都被解绑,所有的事件监听器被移除,所有的子实例也都被销毁。

参考文献: [1](【Vue】详解Vue生命周期 - 腾讯云开发者社区-腾讯云 (tencent.com)) [2](超详细vue生命周期解析(详解) - 百度文库 (baidu.com))
[3](保姆级vue生命周期 - 掘金 (juejin.cn))