guoshuai93 / blog

19 stars 2 forks source link

vue 2.x 打怪之路 — 实例生命周期钩子 #25

Open guoshuai93 opened 6 years ago

guoshuai93 commented 6 years ago

学习 vue 有段时间了,但是对 vue 实例的生命周期还是有些许疑惑,在参考了一些网友的文章后,结合自己的理解,记录下帮助自己更好地学习。

实例生命周期钩子

生命周期图示,摘自官方:

lifecycle

所有的生命周期钩子自动绑定 this 上下文到实例中,因此你可以访问数据,对属性和方法进行运算。这意味着你不能使用箭头函数来定义一个生命周期方法 (例如 created: () => this.fetchTodos()) 。这是因为箭头函数绑定了父上下文,因此 this 与你期待的 Vue 实例不同。

各周期及其描述

生命周期钩子 描述
beforeCreate 实例初始化之后,数据观测 (data observer) 和 event/watcher 事件配置之前被调用。
created 实例创建完成后被立即调用。在这一步,实例已完成以下的配置:数据观测 (data observer),属性和方法的运算,watch/event 事件回调。然而,挂载阶段还没开始,$el 属性目前不可见。
beforeMount 在挂载开始之前被调用:相关的 render 函数首次被调用。
mounted el 被新创建的 vm.$el 替换,并挂载到实例上去之后调用该钩子。(不会承诺所有的子组件也都一起被挂载。如果你希望等到整个视图都渲染完毕,可以用 vm.$nextTick 替换掉 mounted
beforeUpdate 数据更新时调用,发生在虚拟 DOM 重新渲染和打补丁之前。
updated 组件 DOM 已经更新( 不会承诺所有的子组件也都一起被重绘)
activated keep-alive 组件激活时调用。
deactivated keep-alive 组件停用时调用。
beforeDestroy 实例销毁之前调用。在这一步,实例仍然完全可用。
destroyed Vue 实例销毁后调用。调用后,Vue 实例指示的所有东西都会解绑定,所有的事件监听器会被移除,所有的子实例也会被销毁。

对于执行时机和执行顺序,结合上面两项我们就能有个大概了解了。

结合实例深入理解

写一段代码,可以让我们的观察更直观些:

<!DOCTYPE html>
<html>

<head>
  <title></title>
  <script type="text/javascript" src="https://cdn.jsdelivr.net/vue/2.1.3/vue.js"></script>
</head>

<body>

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

  <script type="text/javascript">
    var app = new Vue({
      el: '#app',
      data: {
        message: "What's your name?"
      },
      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)
        console.groupEnd();
      },
      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); //已被初始化
        console.groupEnd();
      },
      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); //已被初始化  
        console.groupEnd();
      },
      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); //已被初始化 
        console.groupEnd();
      },
      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);
        console.groupEnd();
      },
      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);
        console.groupEnd();
      },
      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);
        console.groupEnd();
      },
      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)
        console.groupEnd();
      }
    })
  </script>
</body>

</html>

打印结果以及数据更新:

lifecycle-record

看一眼绿色方框围住的区域,在 beforeMount 时期,DOM 已经部分渲染了,这里就是应用了 虚拟DOM 的技术(Virtual DOM),在 Mounted 时期开始把数据渲染进去。

实践笔记

一些关于实际应用中的东西:

Vue.nextTick

描述: 在下次 DOM 更新循环结束之后执行延迟回调。在修改数据之后立即使用这个方法,获取更新后的 DOM。vue 官网

官方提供了一种写法, vm.$nextTick,用 this 自动绑定到调用它的实例上

created() {
  setTimeout(() => {
    this.number = 100
    this.$nextTick(() => {
      console.log('nextTick', document.getElementsByTagName('p')[0])
    })
  }, 100)
}

在什么时候用? 为什么在 Vue 生命周期的 created() 钩子函数进行的 DOM 操作一定要放在 Vue.nextTick() 的回调函数中?

  1. 原因是在 created() 钩子函数执行的时候 DOM 其实并未进行任何渲染,而此时进行 DOM 操作无异于徒劳,所以此处一定要将 DOM 操作的 js代码放进 Vue.nextTick() 的回调函数中。与之对应的就是 mounted 钩子函数,因为该钩子函数执行时所有的 DOM 挂载和渲染都已完成,此时在该钩子函数中进行任何 DOM 操作都不会有问题 。
  2. 在数据变化后要执行的某个操作,而这个操作需要使用随数据改变而改变的 DOM 结构的时候,这个操作都应该放进 Vue.nextTick() 的回调函数中。

参考

  1. vuejs.org
  2. Vue2.0 探索之路——生命周期和钩子函数的一些理解