Vue.nextTick源码分析

必知的语法

  1. MutationObserver
  2. setImmediate

源码

export let isUsingMicroTask = false

const callbacks = []
let pending = false

function flushCallbacks () {
  pending = false
  const copies = callbacks.slice(0)
  callbacks.length = 0
  for (let i = 0; i < copies.length; i++) {
    copies[i]()
  }
}
/*
  这里我们使用微任务使用异步延迟包装器。
  在2.5中,我们使用了(宏)任务(与微任务结合使用)。但是,当状态在重新绘制之前被更改时,它会有一些微妙的问题 (例如#6813,out-in transitions)。
  此外,在事件处理程序中使用(宏)任务会导致一些奇怪的行为,这是无法规避的
(例如#7109、#7153、#7546、#7834、#8109)。
  所以我们现在到处都在使用微任务。
  这种权衡的一个主要缺点是存在一些场景:
  微任务的优先级过高,并在支持的顺序事件两者之间触发(例如#4521、#6690,它们有解决方案)
  或者甚至是在同一事件(#6566)之间冒泡。 
*/
let timerFunc


/*
  nextTick行为利用了微任务队列,可以通过Promise.then或MutationObserver访问该队列。
  MutationObserver获得了更广泛的支持,但是它受到了严重的干扰,此干扰是在ios> = 9.3.3中的UIWebView触发触摸事件处理程序。触发几次后完全停止工作…因此,如果native Promise可用,我们将使用它: 
*/
/* istanbul ignore next, $flow-disable-line */
if (typeof Promise !== 'undefined' && isNative(Promise)) {
  const p = Promise.resolve()
  timerFunc = () => {
    p.then(flushCallbacks)
    /*
      在有问题的UIWebViews中,Promise.then不会完全中断,
      但它会陷入一种奇怪的状态,即回调被推入微任务队列,但队列没有被刷新,直到浏览器需要做一些其他的工作,比如处理一个计时器。
      因此,我们可以通过添加一个空计时器来“强制”刷新微任务队列。
    */
    if (isIOS) setTimeout(noop)
  }
  isUsingMicroTask = true
} else if (!isIE && typeof MutationObserver !== 'undefined' && (
  isNative(MutationObserver) ||
  // PhantomJS and iOS 7.x
  MutationObserver.toString() === '[object MutationObserverConstructor]'
)) {
  /*  
    在native Promise不可用时使用MutationObserver,  
    例如PhantomJS, iOS7, android4.4  (#6466 MutationObserver在IE11中是不可靠的)    
    MutationObserver是用来监听目标DOM结构是否改变  
  */
  let counter = 1
  const observer = new MutationObserver(flushCallbacks)
  const textNode = document.createTextNode(String(counter))
  observer.observe(textNode, {
    // 文本节点的值更改时是否调用观察者的回调函数
    characterData: true
  })
  timerFunc = () => {
    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 = () => {
    setImmediate(flushCallbacks)
  }
} else {
  // Fallback to setTimeout.
  timerFunc = () => {
    setTimeout(flushCallbacks, 0)
  }
}

export function nextTick (cb?: Function, ctx?: Object) {
  let _resolve
  callbacks.push(() => {
    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(resolve => {
      _resolve = resolve
    })
  }
}

nextTick 原理

主要通过事件循环(Event Loop),Vue在更新Dom时是异步执行,只要侦听到数据变化,Vue将开启一个队列,并缓冲在同一时间循环中发生的所有数据变更.如果同一个watcher被触发多次,只会被推入到队列中一次.这种在缓冲中去掉重复数据对于避免不必要的计算和Dom操作是非常重要的.Vue 在内部对异步队列尝试使用原生的 Promise.then、MutationObserver 和 setImmediate,如果执行环境不支持,则会采用 setTimeout(fn, 0) 代替。

链接

面试题:Vue中$nextTick原理

Copyright: 采用 知识共享署名4.0 国际许可协议进行许可

Links: https://demongao.com/2020/09/vuenexttick源码分析

Buy me a cup of coffee ☕.