vue生命周期(lifecycle)以及对nextTick的理解
每个Vue实例在被创建的时候,都会经历一系列初始化的过程。比如说需要设置数据监听、模板编译、将实例挂载到DOM结构上并且在数据变化时对DOM结构进行更新等等。Vue允许开发者在不同的生命周期运行一些钩子函数(hook),给开发者在不同的生命周期中添加自己代码的机会。所有的生命周期钩子自动绑定 this
上下文到实例中,因此你可以访问数据,对属性和方法进行运算,这也意味着我们不能够用箭头函数来定义生命周期函数。ok,生命周期的定义就到这里,下面看一下vue具体有哪些生命周期,以及再不同的生命周期里面会发生什么事情。
官方给的生命周期钩子函数如下:
#beforeCreate
#created
#beforeMount
#mounted
#beforeUpdate
#updated
#beforeDestroy
#destroyed
#errorCaptured
#activated
#deactivated
在这里钩子函数里面,着重看前八个,后面三个在实际开发中基本涉及不到,如果有兴趣了解可移步到官方文档https://cn.vuejs.org/v2/api/#%E9%80%89%E9%A1%B9-%E7%94%9F%E5%91%BD%E5%91%A8%E6%9C%9F%E9%92%A9%E5%AD%90。
1 var vm = { 2 el: '#app', 3 data: { 4 message: 'hello world.' 5 }, 6 beforeCreate: function() { 7 // 在实例初始化之后,数据观测 (data observer) 和 event/watcher 事件配置之前被调用。 8 // 在这个生命周期里,vue实例还只是空壳,数据和dom结构都还没有加载 9 console.log(this.message); // undefined 10 }, 11 created: function() { 12 // 在实例创建完成后被立即调用。在这一步,实例已完成以下的配置:数据观测 (data observer),属性和方法的运算,watch/event 事件回调。然而,挂载阶段还没开始,$el 目前不可见。 13 // 在这个生命周期里,数据已经加载完成,并且在这里面修改数据并不会出发update函数。 14 // 一般情况下会在这里面做一些异步数据的获取,比如说从服务器获取数据到本地。 15 console.log(this.message); // 'hello world' 16 }, 17 beforeMount: function() { 18 // 在挂载开始之前被调用:相关的 render 函数首次被调用。 19 // 在这里面,虚拟DOM已经加载完毕,但是对应的真实DOM还未加载,在这里面是获取不到真实DOM结点的。 20 // 接下来执行render,加载真实DOM。 21 // 在这里面进行数据修改依旧不会触发update函数。 22 }, 23 mounted: function() { 24 // 在这里面,真实DOM、数据、事件处理已经加载完毕,可以进行对应的修改。 25 // 从这里开始进行数据修改都会触发update函数。 26 }, 27 beforeUpdate: function() { 28 // 数据更新时调用,发生在虚拟 DOM 打补丁之前。这里适合在更新之前访问现有的 DOM,比如手动移除已添加的事件监听器。 29 // 这个没啥好说的,在数据修改(或者说要重新进行渲染)的时候会触发的钩子函数。 30 // 值得注意的是,千万不要在这里面进行数据修改,否则会陷入死循环!道理你自己思考一下就知道为什么了! 31 32 }, 33 updated: function() { 34 // 由于数据更改导致的虚拟 DOM 重新渲染和打补丁,在这之后会调用该钩子。 35 // 当这个钩子被调用时,组件 DOM 已经更新,所以你现在可以执行依赖于 DOM 的操作。然而在大多数情况下,你应该避免在此期间更改状态。如果要相应状态改变,通常最好使用计算属性或 watcher 取而代之。 36 // 同样的,在数据修改完毕之后会调用的钩子函数。 37 // 在这里面一样不能进行数据修改!!!谨记!!! 38 39 }, 40 beforeDestroy: function() { 41 // 实例销毁之前调用。在这一步,实例仍然完全可用。 42 // 销毁前执行(手动使用$destroy方法被调用的时候就会执行),一般在这里善后:清除计时器、清除非指令绑定的事件等等。 43 }, 44 destroyed: function() { 45 // Vue 实例销毁后调用。调用后,Vue 实例指示的所有东西都会解绑定,所有的事件监听器会被移除,所有的子实例也会被销毁。 46 // 组件的数据绑定、监听...都去掉了,只剩下dom空壳,这里也可以善后。 47 } 48 }
上面我们说到,在发生数据修改的时候都会触发beforeUpdate和updated这两个钩子函数,但是如果要对特定的修改执行update函数就会变得很麻烦。官方为此提供了另外一种解决办法,那就是$nextTick。$nextTick()是在下次 DOM 更新循环结束之后执行延迟回调,可以在修改数据之后立即使用这个方法,获取更新后的 DOM。简单的理解是:当数据更新了,在dom中渲染后,自动执行该函数。下面举个简单的例子:
1 <template> 2 <div> 3 <span ref="span">{{ message }}</span> 4 </div> 5 </template> 6 7 <script> 8 export default { 9 name: 'test', 10 data () { 11 return { 12 message: "初始值" 13 } 14 }, 15 methods:{ 16 changeMsg: function(){ 17 this.message = "修改后的值"; 18 console.log(this.$refs.span.innerText); //原始值 19 } 20 }, 21 created() { 22 this.changeMsg(); 23 } 24 } 25 </script>
这是因为在修改完message之后,尚未执行update函数,对应的DOM结构还未发生改变。使用nextTick稍作修改:
1 <template> 2 <div> 3 <span ref="span">{{ message }}</span> 4 </div> 5 </template> 6 7 <script> 8 export default { 9 name: 'test', 10 data () { 11 return { 12 message: "初始值" 13 } 14 }, 15 methods:{ 16 changeMsg: function(){ 17 this.message = "修改后的值"; 18 this.$nextTick(function(){ 19 console.log(that.$refs.span.innerText); //修改后的值 20 }); 21 22 } 23 }, 24 created() { 25 this.changeMsg(); 26 } 27 } 28 </script> 29
因此,在某些有特殊需求的情况下,是可以使用nextTick来代替update进行使用的。那么什么情况下需要使用到nextTick()呢?
一、需要在created()钩子函数进行的DOM操作。因为在created()钩子函数执行时,真实DOM 并未进行任何渲染,而此时进行DOM操作无异于徒劳,所以此处一定要将DOM操作的js代码放进nextTick()的回调函数中。与之对应的就是mounted钩子函数,因为该钩子函数执行时所有的DOM挂载已完成。
二、需要在改变DOM之后基于新的DOM做点什么,对新DOM一系列的js操作都需要放进nextTick()的回调函数中。
Vue.nextTick(callback)的原理:
Vue是异步执行dom更新的,一旦观察到数据变化,Vue就会开启一个队列,然后把在同一个事件循环 (event loop) 当中观察到数据变化的 watcher 推送进这个队列。如果这个watcher被触发多次,只会被推送到队列一次。这种缓冲行为可以有效的去掉重复数据造成的不必要的计算和DOM操作。而在下一个事件循环时,Vue会清空队列,并进行必要的DOM更新。
当你设置 vm.data = 'new value',DOM 并不会马上更新,而是在异步队列被清除,也就是下一个事件循环开始时执行更新时才会进行必要的DOM更新。如果此时你想要根据更新的 DOM 状态去做某些事情,就会出现问题。为了在数据变化之后等待 Vue 完成更新 DOM ,可以在数据变化之后立即使用 Vue.nextTick(callback) 。这样回调函数在 DOM 更新完成后就会调用。