怎么理解NextTick

Vue 官网对NextTick的解释:

NextTick是在下次 DOM 更新循环结束之后执行的延迟回调。在修改数据之后立即使用这个方法,获取更新后的 DOM。尤其是和第三方插件进行配合的时候,免不了要进行DOM操作。而nextTick就提供了一个桥梁,它可以确保我们操作的是更新后的DOM。当Dom元素状态发生变化时重新应用该插件,就会用到该方法,这时候就需要在 $nextTick 的回调函数中执行重新应用插件的方法(例如我们经常使用的Echarts)。这很容易会让我们联想到vue中我们经常提到的生命周期,在 vue 中 created 钩子函数执行的时候, 其实并未进行任何DOM元素的渲染,所以得等DOM更新完毕以后放在 NextTick中去获取 DOM,与其对应的生命周期钩子函数是 mounted,该钩子函数执行时所有的DOM挂载已完成。

Vue.nextTick(callback) 使用原理:

由于Vue是异步执行DOM 更新的,一旦观察到数据变化,Vue就会开启一个队列,然后把在同一个循环事件 (event loop) 当中观察到数据变化的 watcher 推送进这个队列。如果这个watcher被触发多次,只会被推送到队列一次。这种缓冲行为可以有效的去掉重复数据造成的不必要的计算和DOm操作。而在下一个事件循环时,Vue会清空队列,并进行必要的DOM更新。当你设置 vm.someData = 'new value',DOM 并不会马上更新,而是在异步队列被清除,也就是下一个事件循环开始时执行更新时才会进行必要的DOM更新。如果此时你想要根据更新的 DOM 状态去做某些事情,就可能会出现问题。。为了在数据变化之后等待 Vue 完成更新 DOM ,可以在数据变化之后立即使用 Vue.nextTick(callback) 。这样回调函数在 DOM 更新完成后就会调用。

NextTick到底是怎么监听到DOM元素状态的更新呢?

根据我们目前所最熟悉的能监听到DOM节点的属性、文本内容、子节点等的改动的API好像只有MutationObserver(HTML5新增的属性)了,其原理是如果检测到浏览器支持MutationObserver,则创建一个文本节点,监听这个文本节点的改动事件,以此来触发nextTickHandler(也就是DOM更新完毕回调)的执行,后面的代码中,会执行手工修改文本节点属性,这样就能进入到回调函数了。但是Vue到底是不是用MutationObserver来监听DOM更新完毕的呢?这个时候就需要我们来看一看vue源码中实现nextTick的代码了。

// src/core/util/env.js
if (typeof MutationObserver !== 'undefined' && (isNative(MutationObserver) || MutationObser
ver.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)
  }
}

我们会发现我们要监听的是模板中的DOM更新完毕,可是vue为什么自己创建了一个文本节点来监听,难道自己创建的文本节点更新完毕,就能代表其他DOM节点更新完毕吗?显然不对!这就牵扯到JS中的事件循环机制了。

事件循环(Event Loop)

简要来说,在js的运行环境中,事件循环会维护一个或多个任务队列(task queues),将用户点击、页面渲染、脚本执行、网络请求等事件作为任务源往队列中加入任务。这时会有一个持续执行的线程来处理这些任务,每执行完一个就从队列中移除它,这就是一次事件循环了,如下图所示:

怎么理解NextTick_第1张图片

我们平时用setTimeout来执行异步代码,其实就是在任务队列的末尾加入了一个task,待前面的任务都执行完后再执行它。每次event loop的最后,会有一个UI render步骤,也就是更新DOM。标准为什么这样设计呢?考虑下面的代码:

for(let i=0; i<100; i++){

    dom.style.left = i + 'px';

}

浏览器会进行100次DOM更新吗?显然不是的,这样太耗性能了。事实上,这100次for循环同属一个task,浏览器只在该task执行完后进行一次DOM更新。

那我们的思路就来了:只要让nextTick里的代码放在UI render步骤后面执行,岂不就能访问到更新后的DOM了?

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

microtask

从名字看,我们可以把它称为微任务。对应的,task队列中的任务也被叫做macrotask。名字相似,性质可不一样了。每一次事件循环都包含一个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用MO是想利用它的microtask特性,而不是想做DOM监听?对喽,就是这样的。核心是microtask,用不用MO都行的。事实上,vue在2.5版本中已经删去了MO相关的代码,因为它是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了,尽管它有执行延迟,可能造成多次渲染,算是没有办法的办法了。

总结

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

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

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

 

你可能感兴趣的:(前端小知识点,vue,nextTick的执行原理)