Vue.js专区

关于vue的nextTick

2018-05-30  本文已影响68人  勤奋的大鱼

  在vue的代码中,有时候会用到this.$nextTick,这个方法的回调函数里可以获取到数据更新之后的DOM,使用的方法这里就不说了。在vue中可以简单理解为nextTick是下一次dom更新。
  另外,在我之前写的源码中,关于Watcher中的update方法只写了简单的同步更新,在vue的源码中,update方法,除非watcher中指定了sync为true,否则都为异步更新。异步更新会通过queueWatcher把当前观察者加入一个全局的watcher队列中,该队列中的watcher都会在nextTick后统一调用。

事件循环(关于macroTask与microTask)

  简单来说,事件循环是这样的,先执行一个macroTask,然后执行完microTask队列中所有的microTask,然后再去执行一个macroTask。
  macroTask有这几种:setTimeout,setInterval,setImmediate,postMessage,MessageChannel,I/O,UI渲染等。
  microTask有这几种:原生Promise,MutationObserver,process.nextTick(Node环境)
看了下源码,在vue2.5.0之前,vue对nextTick的处理都是这样的:
1.是否有原生的Promise对象,如果是,就使用Promise.then异步触发nextTick
2.如果不支持Promise,就使用MutationObserver来异步触发nextTick
3.如果都不支持,则使用setTimeout。
  总而言之,从大部分的浏览器来说,所有的nextTick都是一个microTask。
  但是,如果去看最新的vue源码,就会发现独立了一个next-tick.js文件出来,里面的逻辑有了一些改变。
至于改动的原因,源码中也有说明,我试着翻译了一下:

// Here we have async deferring wrappers using both microtasks and (macro) tasks.
// In < 2.4 we used microtasks everywhere, but there are some scenarios where
// microtasks have too high a priority and fire in between supposedly
// sequential events (e.g. #4521, #6690) or even between bubbling of the same
// event (#6566). However, using (macro) tasks everywhere also has subtle problems
// when state is changed right before repaint (e.g. #6813, out-in transitions).
// Here we use microtask by default, but expose a way to force (macro) task when
// needed (e.g. in event handlers attached by v-on).
// 
// 这里我们的异步函数一起使用了microtasks和macrotasks,
// 在2.4版本之前,我们在任何地方都是用了microtasks,但是这里是一些microtasks有错误的场景
// 由于microtasks有着太高的优先级,它在两次本应该顺序执行的事件之间触发了(比如 #4521, #6690),
// 甚至在同一个事件冒泡的过程之间触发了(比如#6566)。
// 然而,所有的地方都使用macroTask也会有一些小问题,比如当状态刚好在浏览器重绘之前改变(#6813)
// 因此,我们默认使用了microtask,但是提供了一个方法强制使用macroTask如果需要的话。(比如在事件的绑定中)

关于事件冒泡的情况,我写了如下一个小demo:

  <div id="a">
    aaaaa
    <div id="b">bbbbb</div>
  </div>
  <script>
    var $a = document.querySelector('#a')
    var $b = document.querySelector('#b')
    $b.addEventListener('click', function () {
      Promise.resolve()
        .then(function () {
          console.log('microTask')
        })
      console.log('b')
    })
    $a.addEventListener('click', function () {
      console.log('a')
    })
  </script>

  按理说nextTick应该在点击完b然后更新完DOM之后执行,但是如果使用了microTask,就像上面的代码,输出的结果为:b,microTask,a,也就是说冒泡到a上面的事件里如果有状态的改变,那么在nextTick中便无法获得,其余的场景原理应该也是差不多。如果把这里的microTask换成一个macroTask,那么在nextTick中就能获得这两个事件之后的DOM。另外,据我推测,涉及到事件的,不管是冒泡还是捕获的,应该都是一个macroTask。
  说回源码,为了解决这个问题,vue2.5之后,next-tick.js中有这样一段代码:

export function withMacroTask (fn: Function): Function {
  return fn._withTask || (fn._withTask = function () {
    useMacroTask = true
    const res = fn.apply(null, arguments)
    useMacroTask = false
    return res
  })
}

 这个方法的作用是,回调函数(fn)中的状态改变引起的nextTick操作,异步任务使用macroTask方法,具体的实现可以参照源码:源码。全局去搜索withMacroTask,可以发现目前只有events模块使用了这个方法。
 另外,最新的源码中,使用的macroTask的优先级为:setImmediate(只有IE支持),MessageChannel,setTimeout

上一篇下一篇

猜你喜欢

热点阅读