vue须知(5)—— 你知道nextTick吗,它是干什么的,实现原理是什么?

13,522 阅读6分钟

这道题考查大家对vue异步更新队列的理解,有一定深度,如果能够很好回答此题,对面试效果有极大帮助

答题思路
1.nextTick是啥?下一个定义
2.为什么需要它呢?用异步更新队列实现原理解释
3我再什么地方用它呢?抓抓头,想想你在平时开发中使用它的地方
4.下面介绍一下如何使用nextTick
5最后能说出源码实现就会显得你格外优秀

先看看官方定义

Vue.nextTick([callback,context])
在下次DOM更新循环结束之后执行延迟回调。在修改数据之后立即使用这个方法,获取更新之后的DOM

// 修改数据 
vm.msg = 'Hello' 
// DOM 还没有更新 
Vue.nextTick(function () { // DOM 更新了 
}) 
  
  • vue如何检测到DOM更新完毕呢?
    能监听到DOM改动的API:MutationObserver

  • 理解MutationObserver
    MutationObserver是HTML5新增的属性,用于监听DOM修改事件,能够监听到节点的属性、文本内 容、子节点等的改动,是一个功能强大的利器。

//MutationObserver基本用法 
var observer = new MutationObserver(function(){   //这里是回调函数 
  console.log('DOM被修改了!'); 
}); 
var article = document.querySelector('article'); 
observer.observer(article); 

vue是不是用MutationObserver来监听DOM更新完毕的呢?
vue的源码中实现nextTick的地方

//vue@2.2.5 /src/core/util/env.js 
if (typeof MutationObserver !== 'undefined' && (isNative(MutationObserver) ||  MutationObserver.toString() === '[object MutationObserverConstructor]')) { 
  var counter = 1 
  var observer = new MutationObserver(nextTickHandler) 
  var textNode = document.createTextNode(String(counter)) 
  observer.observe(textNode, { 
      characterData: true 
  }) 
  timerFunc = () => { 
    counter = (counter + 1) % 2 
    textNode.data = String(counter) 
  } 
} 
  • 事件循环(Event Loop)
    在js的运行环境中,通常伴随着很多事件的发生,比如用户点击、页面渲染、脚本执行、网络请求,等等。为了协调这些事件的处理,浏览器使用事件循环机制。
    简要来说,事件循环会维护一个或多个任务队列(task queues),以上提到的事件作为任务源往队列 中加入任务。有一个持续执行的线程来处理这些任务,每执行完一个就从队列中移除它,这就是一次事件循环。

image.png

for(let i=0; i<100; i++){ 
    dom.style.left = i + 'px'; 
} 

事实上,这100次for循环同属一个task,浏览器只在该task执行完后进行一次DOM更新。 只要让nextTick里的代码放在UI render步骤后面执行,岂不就能访问到更新后的DOM了?

vue就是这样的思路,并不是用MO进行DOM变动监听,而是用队列控制的方式达到目的。
那么vue又是如何做到队列控制的呢?我们可以很自然的想到setTimeout,把nextTick要执行的代码 当作下一个task放入队列末尾。
vue的数据响应过程包含:数据更改->通知Watcher->更新DOM。而数据的更改不由我们控制, 可能在任何时候发生。如果恰巧发生在重绘之前,就会发生多次渲染。这意味着性能浪费,是vue 不愿意看到的。
所以,vue的队列控制是经过了深思熟虑的。在这之前,我们还需了解event loop的另一个重要概 念,microtask。

  • microtask
    从名字看,我们可以把它称为微任务。

每一次事件循环都包含一个microtask队列,在循环结束后会依次执行队列中的microtask并移除,然后 再开始下一次事件循环。
在执行microtask的过程中后加入microtask队列的微任务,也会在下一次事件循环之前被执行。也就是说macrotask总要等到microtask都执行完后才能执行,microtask有着更高的优先级。
microtask的这一特性,是做队列控制的最佳选择。vue进行DOM更新内部也是调用nextTick来做异步 队列控制。而当我们自己调用nextTick的时候,它就在更新DOM的那个microtask后追加了我们自己的 回调函数,从而确保我们的代码在DOM更新后执行,同时也避免了setTimeout可能存在的多次执行问 题。
常见的microtask有:Promise、MutationObserver、Object.observe(废弃),以及nodejs中的 process.nextTick.

看到了MutationObserver,vue用MutationObserver是想利用它的microtask特性,而不是想做DOM 监听。核心是microtask,用不用MutationObserver都行的。事实上,vue在2.5版本中已经删去了 MutationObserver相关的代码,因为它是HTML5新增的特性,在iOS上尚有bug。
那么最优的microtask策略就是Promise了,而令人尴尬的是,Promise是ES6新增的东西,也存在兼容 问题呀。所以vue就面临一个降级策略。

  • vue的降级策略
    上面我们讲到了,队列控制的最佳选择是microtask,而microtask的最佳选择是Promise.但如果当前环 境不支持Promise,vue就不得不降级为macrotask来做队列控制了。

macrotask有哪些可选的方案呢?前面提到了setTimeout是一种,但它不是理想的方案。因为 setTimeout执行的最小时间间隔是约4ms的样子,略微有点延迟。

在vue2.5的源码中,macrotask降级的方案依次是:setImmediate、MessageChannel、setTimeout. setImmediate是最理想的方案了,可惜的是只有IE和nodejs支持。

MessageChannel的onmessage回调也是microtask,但也是个新API,面临兼容性的尴尬。

所以最后的兜底方案就是setTimeout了,尽管它有执行延迟,可能造成多次渲染,算是没有办法的办法 了。

总结:

以上就是vue的nextTick方法的实现原理了,总结一下就是:

  1. vue用异步队列的方式来控制DOM更新和nextTick回调先后执行

  2. microtask因为其高优先级特性,能确保队列中的微任务在一次事件循环前被执行完毕

  3. 因为兼容性问题,vue不得不做了microtask向macrotask的降级方案

下面的话不是人看的

1.nextTick是Vue提供的一个全局API由于vue的异步更新策略导致我们对数据的修改不会立刻体现在dom变化上,此时如果想要立即获取更新后的dom状态,就需要使用这个方法

2.Vue在更新DOM时是异步执行的。只要侦听到数据变化,Vue将开启一个队列,并缓冲在同一事件循环中发 生的所有数据变更。如果同一个watcher被多次触发,只会被推入到队列中一次。这种在缓冲时去除重复数据对于避免不必要的计算和DOM操作是非常重要的。nextTick方法会在队列中加入一个回调函数,确保该函数在前面的dom操作完成后才调用。

3.所以当我们想在修改数据后立即看到dom执行结果就需要用到nextTick方法。

4比如,我在干什么的时候就会使用nextTick传一个回调函数进去,在里面执行dom操作即可。

5.我也有简单了解nextTick实现,它会在callbacks里面加入我们传入的函数然后用timerFunc异步方式调用它 们,首选的异步方式会是Promise。这让我明白了为什么可以在nextTick中看到dom操作结果。