你知道nextTick的原理吗?

339 阅读6分钟

这个问题考察对vue异步更新队列/异步更新策略的理解

答题思路:

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

示例:

  1. nextTick 是 vue 提供的一个全局 API,由于 vue 的异步更新策略导致我们对数据的修改不会立即体现在 dom 变化上,此时如果想要立即获取更新后的 dom 状态,就需要使用这个方法。
  2. vue 在更新 dom 时是异步执行的。只要侦听到数据变化,vue将开启一个队列,并缓存在同一事件循环中发生的所以数据变更。如果同一个 watcher 被多次触发,只会被推入到队列中一次。这种在缓冲时去除重复数据对于避免不必要的计算和 DOM 操作是非常重要的。nextTick 方法会在队列中加入一个回调函数,确保该函数在前面的 dom 操作完成后才调用。
  3. 所以当我们想要在修改数据后立即看到 dom 执行结果就需要用到 nextTick 方法。
  4. 比如,我们在干什么的时候就会使用 nextTick,传一个回调函数进去,在里面执行 dom 操作即可。
  5. 我也有简单了解 nextTick 的实现,它会在 callback 里面加入我们传入的函数,然后用 timerFunc 异步方式调用它们,首选的异步方式会是 Promise。这让我明白了为什么可以在nextTick中看到 dom 操作结果。

先看看官方定义

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

// 修改数据 
vm.msg = 'Hello' 
// DOM 还没有更新 
Vue.nextTick(function () { 
     // DOM 更新了 
})

尽管MVVM框架并不推荐访问DOM,但有时候确实会有这样的需求,尤其是和第三方插件进行配合的 时候,免不了要进行DOM操作。而nextTick就提供了一个桥梁,确保我们操作的是更新后的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) 
      } 
    }
    
  • 事件循环(EventLoop)

    • 在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的降级方案