![89ced5722acdf0a75f8a851c1bca88ad.png](https://i-blog.csdnimg.cn/blog_migrate/ea42b9243478f54c180fb096e929ef29.jpeg)
使用场景:
在我们开发项目的时候,总会碰到一些场景:当我们使用vue操作更新dom后,需要对新的dom做一些操作时,但是这个时候,我们往往会获取不到更新后的DOM.因为这个时候,dom还没有重新渲染,所以我们就要使用vm.$nextTick方法。
用法:
nextTick接受一个回调函数作为参数,它的作用将回调延迟到下次DOM更新周期之后执行。
methods:{example:function(){ //修改数据 this.message='changed'//此时dom还没有跟新,不能获取新的数据 this.$nextTick(function(){ //dom现在跟新了 //可以获取新的dom数据,执行操作 this.doSomeThing() }) }}复制代码
小思考:
在用法中,我们发现,什么是下次DOM更新周期之后执行,具体是什么时候,所以,我们要明白什么是DOM更新周期。 在Vue当中,当视图状态发生变化时,watcher会得到通知,然后触发虚拟DOM的渲染流程,渲染这个操作不是同步的,是异步。Vue中有一个队列,每当渲染时,会将watcher推送这个队列,在下一次事件循环中,让watcher触发渲染流程。
为什么Vue使用异步更新队列?
简单来说,就是提升性能,提升效率。 我们知道Vue2.0使用虚拟dom来进行渲染,变化侦测的通知只发送到组件上,组件上的任意一个变化都会通知到一个watcher上,然后虚拟DOM会对整个组件进行比对(diff算法,以后有时间我会详细研究一下),然后更新DOM.如果在同一轮事件循环中有两个数据发生变化了,那么组件的watcher会收到两次通知,从而进行两次渲染(同步更新也是两次渲染),事实上我们并不需要渲染这么多次,只需要等所有状态都修改完毕后,一次性将整个组件的DOM渲染到最新即可。
如何解决一次事件循环组件多次状态改变只需要一次渲染更新?
其实很简单,就是将收到的watcher实例加入队列里缓存起来,并且再添加队列之前检查这个队列是否已存在相同watcher。不存在时,才将watcher实例添加到队列中。然后在下一次事件循环中,Vue会让这个队列中的watcher触发渲染并清空队列。这样就保证一次事件循环组件多次状态改变只需要一次渲染更新。
什么是事件循环?
我们知道js是一门单线程非阻塞的脚本语言,意思是执行js代码时,只有一个主线程来处理所有任务。非阻塞是指当代码需要处理异步任务时,主线程会挂起(pending),当异步任务处理完毕,主线程根据一定的规则去执行回调。事实上,当任务执行完毕,js会将这个事件加入一个队列(事件队列)。被放入队列中的事件不会立刻执行其回调,而是当前执行栈中所有任务执行完毕后,主线程会去查找事件队列中是否有任务。 异步任务有两种类型,微任务和宏任务。不同类型的任务会被分配到不同的任务队列中。 执行栈中所有任务执行完毕后,主线程会去查找事件队列中是否有任务,如果存在,依次执行所有队列中的回调,直到为空。然后再去宏任务队列中取出一个事件,把对应的回调加入当前执行栈,当前执行栈中所有任务都执行完毕,检查微任务队列是否有事件。无线循环此过程,叫做事件循环。
常见的微任务
- Promise.then
- Object.observe
- MutationObserver
常见的宏任务
- setTimeout
- setInterval
- setImmediate
- UI交互事件
在我们使用vm.$nextTick中获取更新后DOM时,一定要在更改数据的后面使用nextTick注册回调。
methods:{example:function(){ //修改数据 this.message='changed'//此时dom还没有跟新,不能获取新的数据 this.$nextTick(function(){ //dom现在跟新了 //可以获取新的dom数据,执行操作 this.doSomeThing() }) }}复制代码
如果是先使用nextTick注册回调,然后修改数据,在微任务队列中先执行使用nextTick注册的回调,然后才执行跟新DOM的回调,所以回调中得不到新的DOM,因为还没有更新。
methods:{example:function(){//此时dom还没有跟新,不能获取新的数据 this.$nextTick(function(){ //dom没有跟新,不能获取新的dom this.doSomeThing() }) //修改数据 this.message='changed' }}复制代码
我们知道,添加微任务队列中的任务执行机制要高于宏任务的执行机制(下面代码必须理解)
methods:{example:function(){//先试用setTimeout向宏任务中注册回调setTimeout(()=>{//现在DOM已经跟新了,可以获取最新DOM}) //然后修改数据 this.message='changed' }}复制代码
setTimeout属于宏任务,使用它注册回调会加入宏任务中,宏任务执行要比微任务晚,所以即便是先注册,也是先跟新DOM后执行setTineout中设置回调。
理解nextTick的作用后,我们以下来介绍实现原理
实现原理剖析:
由于nextTick会将回调添加到任务队列中延迟执行,所以在回调执行之前,如果反复使用nextTick,Vue并不会将回调添加到任务队列中,只会添加一个任务。Vue内部有一个列表来存储nextTick参数中提供的回调,当任务触发时,以此执行列表里的所有回调并清空列表,其代码如下(简易版):
const callbacks=[]let pending=falsefunction flushCallBacks(){ pending=false const copies=callbacks.slice(0) callbacks.length=0 for(let i=0;i{ p.then(flushCallBacks)}export function nextTick(cb,ctx){ callbacks.push(()=>{ if(cb){ cb.call(ctx) } }) if(!pending){ pending=true microTimeFun() }}复制代码
理解相关变量:
- callbacks:用来存储用户注册的回调函数(获得了更新后DOM所进行的操作)
- pending:用来标记是否向任务队列添加任务,pending为false,表示任务队列没有nextTIck任务,需要添加nextTick任务,当添加一个nextTick任务时,pending为ture,在回调执行之前还有nextTick时,并不会重复添加任务到任务队列,当回调函数开始执行时,pending为flase,进行新的一轮事件循环。
- flushCallbacks:就是我们所说的被注册在任务队列中的任务,当这个函数执行,callbacks中所有函数依次执行,然后清空callbacks,并重置pending为false,所以说,一轮事件循环中,flushCallbacks只会执行一次。
- microTimerFunc:它的作用就是使用Promise.then将flushCallbacks添加到微任务队列中。
下图给出nextTick内部注册流程和执行流程。
![e9d8448756dac24168e5fa5ed219036e.png](https://i-blog.csdnimg.cn/blog_migrate/66efee2d44633855983e6fb9a86dddd0.jpeg)
官方文档里面还有这么一句话,如果没有提供回调且支持Promise的环境下,则返回一个Promise。也就是说。可以这样使用nextTick
this.$nextTick().then(function(){ //dom跟新了})复制代码
要实现这个功能,只需要在nextTIck中判断,如果没有提供回调且当前支持Promise,那么返回Promise,并且在callbacks中添加一个函数,当这个函数执行时,执行Promise的resolve,即可,代码如下
function nextTick (cb, ctx) { var _resolve; callbacks.push(function () { if (cb) { cb.call(ctx); } else if (_resolve) { _resolve(ctx); } }); if (!pending) { pending = true; timerFunc(); } if (!cb && typeof Promise !== 'undefined') { return new Promise(function (resolve) { _resolve = resolve; }) } }复制代码
nextTick源码查看
到此,nextTick原理基本上已经讲完了。那我们现在可以看看真正vue中关于nextTick中的源码,大概我们都能理解的过来了,源码如下。
var timerFunc; // The nextTick behavior leverages the microtask queue, which can be accessed // via either native Promise.then or MutationObserver. // MutationObserver has wider support, however it is seriously bugged in // UIWebView in iOS >= 9.3.3 when triggered in touch event handlers. It // completely stops working after triggering a few times... so, if native // Promise is available, we will use it: /* istanbul ignore next, $flow-disable-line */ if (typeof Promise !== 'undefined' && isNative(Promise)) { var p = Promise.resolve(); timerFunc = function () { p.then(flushCallbacks); // In problematic UIWebViews, Promise.then doesn't completely break, but // it can get stuck in a weird state where callbacks are pushed into the // microtask queue but the queue isn't being flushed, until the browser // needs to do some other work, e.g. handle a timer. Therefore we can // "force" the microtask queue to be flushed by adding an empty timer. if (isIOS) { setTimeout(noop); } }; isUsingMicroTask = true; } else if (!isIE && typeof MutationObserver !== 'undefined' && ( isNative(MutationObserver) || // PhantomJS and iOS 7.x MutationObserver.toString() === '[object MutationObserverConstructor]' )) { // Use MutationObserver where native Promise is not available, // e.g. PhantomJS, iOS7, Android 4.4 // (#6466 MutationObserver is unreliable in IE11) var counter = 1; var observer = new MutationObserver(flushCallbacks); var textNode = document.createTextNode(String(counter)); observer.observe(textNode, { characterData: true }); timerFunc = function () { counter = (counter + 1) % 2; textNode.data = String(counter); }; isUsingMicroTask = true; } else if (typeof setImmediate !== 'undefined' && isNative(setImmediate)) { // Fallback to setImmediate. // Technically it leverages the (macro) task queue, // but it is still a better choice than setTimeout. timerFunc = function () { setImmediate(flushCallbacks); }; } else { // Fallback to setTimeout. timerFunc = function () { setTimeout(flushCallbacks, 0); }; } function nextTick (cb, ctx) { var _resolve; callbacks.push(function () { if (cb) { try { cb.call(ctx); } catch (e) { handleError(e, ctx, 'nextTick'); } } else if (_resolve) { _resolve(ctx); } }); if (!pending) { pending = true; timerFunc(); } // $flow-disable-line if (!cb && typeof Promise !== 'undefined') { return new Promise(function (resolve) { _resolve = resolve; }) } }复制代码
总结
这篇文章大概花了两天时间才写出来的,充分的参考了这本书,充分了理解书上关于vm.$nextTick中的每一句话,同时也对js中的事件循环有了进一步认识,对js运行机制也进一步加深。作为前端小白,不想只局限于调用各种API,更要知道其原理,每天进步一小步。希望大家能多多与我讨论交流。