源碼閱讀:Vue.nextTick()

1. 知識儲備

在閱讀源代碼之前請按順序閱讀這些文章/視頻:
Vue.js:異步更新隊列
從瀏覽器多進程到JS單線程,JS運行機制最全面的一次梳理
Philip Roberts: What the heck is the event loop anyway? | JSConf EU 2014
MDN:MutationObserver
MDN:MessageChannel
Tasks, microtasks, queues and schedules
Vue.js 升級踩坑小記(可省略,但是這篇文章給我的收獲還是很大的)

2. 知識點小結(jié):

這里只做一個最簡單粗暴的知識點小結(jié),不包含任何解釋。
宏任務(macrotask)
主代碼塊,setTimeout,setInterval, setImmediate,MessageChannel,postMessage

微任務(microtask)
promise,MutationObserver

任務執(zhí)行順序以及渲染的執(zhí)行
macrotask -> microtask -> 渲染 -> macrotask -> microtask -> 渲染 -> ......

3. Vue如何實現(xiàn) .nextTick()

Vue 在內(nèi)部嘗試對異步隊列使用原生的 Promise.then 和 MessageChannel,如果執(zhí)行環(huán)境不支持,會采用 setTimeout(fn, 0) 代替。

異步隊列很明顯提高了性能,但是如果我們想要在DOM更新之后做點什么,可能就有點麻煩了(詳情看這),因為主代碼塊在微任務列表之前,而Vue是在微任務或者下一個宏任務中才更新DOM的,這時候就需要使用.nextTick()了。

Vue.js 2.5之前,幾乎都是用 microtask 來模擬 Node.js 的.nextTick()

  1. 瀏覽器是否支持Promise?是則使用Promise,否則進行下一步
  2. 瀏覽器是否支持MutationObserver,是則使用MutationObserver,否則進行下一步
  3. setTimeout (此時是一個macrotask)

Vue.js 2.5之后,默認使用 microtask ,在DOM事件強制使用 macrotask:

  1. 先確定使用 macrotask 時用哪個API,優(yōu)先級為:
    setImmediate -> MessageChannel ->setTimeout
  2. 確定使用 microtask 時用哪個API,優(yōu)先級為:Promise -> macroTimerFunc(和macrotask一致)
  3. 判斷是否使用 macrotask ,是則調(diào)用macroTimerFunc,否則調(diào)用 microTimerFunc
  4. DOM事件默認會包裹一層函數(shù)來強制其使用 macrotask

4. 正題

Vue.js 2.5之前,.nextTick()放在env.js中,使用Promise, MutationObserver, setTimeout來實現(xiàn)異步隊列:

// env.js

/* @flow */
/* globals MutationObserver */

import { noop } from 'shared/util'

// 能否使用 __proto__?
export const hasProto = '__proto__' in {}

// 瀏覽器環(huán)境檢測,和本文無關(guān),可忽略
export const inBrowser = typeof window !== 'undefined'
export const UA = inBrowser && window.navigator.userAgent.toLowerCase()
export const isIE = UA && /msie|trident/.test(UA)
export const isIE9 = UA && UA.indexOf('msie 9.0') > 0
export const isEdge = UA && UA.indexOf('edge/') > 0
export const isAndroid = UA && UA.indexOf('android') > 0
export const isIOS = UA && /iphone|ipad|ipod|ios/.test(UA)

// this needs to be lazy-evaled because vue may be required before
// vue-server-renderer can set VUE_ENV
let _isServer
export const isServerRendering = () => {
  if (_isServer === undefined) {
    /* istanbul ignore if */
    if (!inBrowser && typeof global !== 'undefined') {
      // detect presence of vue-server-renderer and avoid
      // Webpack shimming the process
      _isServer = global['process'].env.VUE_ENV === 'server'
    } else {
      _isServer = false
    }
  }
  return _isServer
}

export const devtools = inBrowser && window.__VUE_DEVTOOLS_GLOBAL_HOOK__

/* istanbul ignore next */
function isNative (Ctor: Function): boolean {
  return /native code/.test(Ctor.toString())
}


// 相關(guān)代碼在這里
export const nextTick = (function () {
  const callbacks = [] // 存放回調(diào)函數(shù)
  let pending = false // 是否有異步隊列(callbacks)正在等待執(zhí)行
  let timerFunc // 處理異步隊列的函數(shù)(Promise,MutationObserver,setTimeout)

  function nextTickHandler () { // 清空callbacks列表,執(zhí)行callback列表中的函數(shù)
    pending = false // 表示沒有異步隊列在等待了
    const copies = callbacks.slice(0)
    callbacks.length = 0
    for (let i = 0; i < copies.length; i++) {
      copies[i]()
    }
  }

  // nextTick利用了微任務隊列,微任務隊列可以用原生的Promise或者MutationObserver來實現(xiàn)
  // MutationObserver被廣泛支持,但是在iOS >= 9.3.3上會有嚴重的bug。
  // 因此優(yōu)先使用Promise
  /* istanbul ignore if */
  if (typeof Promise !== 'undefined' && isNative(Promise)) { // 用Promise把回調(diào)函數(shù)推入微任務隊列
    var p = Promise.resolve()
    var logError = err => { console.error(err) }
    timerFunc = () => {
      p.then(nextTickHandler).catch(logError)
      // 在UIWebViews中雖然Promise.then沒有完全break,但是會陷入一個很奇怪的狀態(tài)
      //回調(diào)函數(shù)都被推入微任務隊列中,但是在瀏覽器處理別的任務(比如timer)之前隊列不會被清空。
      // 因此添加一個空的timer來強制清空微任務隊列。
      if (isIOS) setTimeout(noop)
    }
  } else if (typeof MutationObserver !== 'undefined' && ( 
    isNative(MutationObserver) ||
    // PhantomJS and iOS 7.x
    MutationObserver.toString() === '[object MutationObserverConstructor]'
  )) {
    // Promise不能用則用MutationObserver,MutationObserver也屬于微任務
    // e.g. PhantomJS IE11, iOS7, Android 4.4
    var counter = 1
    var observer = new MutationObserver(nextTickHandler)
    var textNode = document.createTextNode(String(counter)) // 創(chuàng)建一個看不見的文本節(jié)點,讓MutationObserver來監(jiān)聽它
    observer.observe(textNode, {
      characterData: true
    })
    timerFunc = () => {
      counter = (counter + 1) % 2
      textNode.data = String(counter)
    }
  } else { // Promise和MutationObserver都不能用
    // 用setTimeout代替,setTimeout為宏任務
    /* istanbul ignore next */
    timerFunc = () => {
      setTimeout(nextTickHandler, 0)
    }
  }

  return function queueNextTick (cb?: Function, ctx?: Object) { // 添加回調(diào)函數(shù),調(diào)用VUe.nextTick即調(diào)用這個函數(shù),注意到可以傳入一個對象做為該函數(shù)的上下文!
    let _resolve
    callbacks.push(() => { // 包裹傳入的函數(shù),綁定其上下文,并push到callbacks中
      if (cb) cb.call(ctx)
      if (_resolve) _resolve(ctx)
    })
    if (!pending) { // 如果沒有異步隊列在等待執(zhí)行,那么處理當前的異步隊列
      pending = true
      timerFunc()
    }
    if (!cb && typeof Promise !== 'undefined') {
      return new Promise(resolve => {
        _resolve = resolve
      })
    }
  }
})()

let _Set
/* istanbul ignore if */
if (typeof Set !== 'undefined' && isNative(Set)) { // 瀏覽器支持Set
  // use native Set when available.
  _Set = Set
} else { // 瀏覽器不支持Set
  // a non-standard Set polyfill that only works with primitive keys.
  _Set = class Set {
    set: Object;
    constructor () {
      this.set = Object.create(null)
    }
    has (key: string | number) { // set[key]是否存在
      return this.set[key] === true
    }
    add (key: string | number) { // 添加一個元素
      this.set[key] = true
    }
    clear () { // 清空對象內(nèi)所有元素
      this.set = Object.create(null)
    }
  }
}

export { _Set }

?
Vue.js 2.5+nestTick單獨成一個文件了:next-tick.js:

// next-tick.js

/* @flow */
/* globals MessageChannel */

import { noop } from 'shared/util'
import { handleError } from './error'
import { isIOS, isNative } from './env'

const callbacks = [] // 存儲回調(diào)函數(shù)
let pending = false // 當前是否有異步隊列在等待執(zhí)行?

function flushCallbacks () { // 執(zhí)行任務隊列中的回調(diào)函數(shù)
  pending = false // pending為false,表示異步隊列已經(jīng)被清空
  const copies = callbacks.slice(0)
  callbacks.length = 0
  for (let i = 0; i < copies.length; i++) {
    copies[i]()
  }
}

// 在<2.4 的版本,nextTick幾乎都是使用microtasks來實現(xiàn)
// 但這會導致一些問題(下面講)
// 所以 2.5+ 默認使用microtasks,但某些場景下會強制使用 macrotasks(比如,v-on綁定的事件)
let microTimerFunc
let macroTimerFunc
let useMacroTask = false // 是否使用macrotask來處理nextTick?默認為否

// 決定macrotask的實現(xiàn)。
//  在技術(shù)上 setImmediate 是最理想的,但是它只能在IE中使用。
// 讓回調(diào)函數(shù)始終排隊在 同一個事件循環(huán)中觸發(fā)的DOM事件 之后的唯一polyfill就是使用MessageChannel
/* istanbul ignore if */
if (typeof setImmediate !== 'undefined' && isNative(setImmediate)) { // 優(yōu)先使用 setImmediate 
  macroTimerFunc = () => {
    setImmediate(flushCallbacks)
  }
} else if (typeof MessageChannel !== 'undefined' && (
  isNative(MessageChannel) ||
  // PhantomJS
  MessageChannel.toString() === '[object MessageChannelConstructor]'
)) {
  const channel = new MessageChannel()
  const port = channel.port2
  channel.port1.onmessage = flushCallbacks
  macroTimerFunc = () => {
    port.postMessage(1)
  }
} else { // 不支持 setImmediate 和 MessageChannel 時用 setTimeout 代替
  /* istanbul ignore next */
  macroTimerFunc = () => {
    setTimeout(flushCallbacks, 0)
  }
}

// 決定MicroTask的實現(xiàn)。
/* istanbul ignore next, $flow-disable-line */
if (typeof Promise !== 'undefined' && isNative(Promise)) {
  const p = Promise.resolve()
  microTimerFunc = () => {
    p.then(flushCallbacks)
    // 在UIWebViews中雖然Promise.then沒有完全break,但是會陷入一個很奇怪的狀態(tài)
    // 回調(diào)函數(shù)都被推入微任務隊列中,但是在瀏覽器處理別的任務(比如timer)之前不會執(zhí)行這些任務
    // 因此添加一個空的timer來強制清空微任務隊列。
    if (isIOS) setTimeout(noop)
  }
} else {
  // 不支持Promise則用macrotask代替
  // MutationObeserver因為兼容性問題被拋棄了
  microTimerFunc = macroTimerFunc
}


// 包裹一個函數(shù),強制其使用macrotask
// 默認會給每一個DOM事件的回調(diào)函數(shù)調(diào)用withMacroTask 
export function withMacroTask (fn: Function): Function {
  return fn._withTask || (fn._withTask = function () {
    useMacroTask = true // 使用macrotask
    const res = fn.apply(null, arguments)
    useMacroTask = false // 狀態(tài)重新設(shè)置為false,不然其他回調(diào)函數(shù)也會用macrotask
    return res
  })
}

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
    if (useMacroTask) {
      macroTimerFunc()
    } else {
      microTimerFunc()
    }
  }
  // $flow-disable-line
  if (!cb && typeof Promise !== 'undefined') {
    return new Promise(resolve => {
      _resolve = resolve
    })
  }
}

5. 為什么默認使用 microtask 的一點補充

(摘自知乎,原鏈接:Vue 中如何使用 MutationObserver 做批量處理?

根據(jù)HTML Standard,在每個 task 運行完以后,UI 都會重渲染(知識點小結(jié)那塊有說到任務執(zhí)行順序以及什么時候渲染),那么在 microtask 中就完成數(shù)據(jù)更新,當前 task 結(jié)束就可以得到最新的 UI 了。反之如果新建一個 task 來做數(shù)據(jù)更新,那么渲染就會進行兩次。(當然,瀏覽器實現(xiàn)有不少不一致的地方,上面 Jake 那篇文章里已經(jīng)有提到。)

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
【社區(qū)內(nèi)容提示】社區(qū)部分內(nèi)容疑似由AI輔助生成,瀏覽時請結(jié)合常識與多方信息審慎甄別。
平臺聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點,簡書系信息發(fā)布平臺,僅提供信息存儲服務。

相關(guān)閱讀更多精彩內(nèi)容

友情鏈接更多精彩內(nèi)容