[vue源碼][nextTick]原理以及源碼解析

nextTick Vue中的nextTick涉及到Vue中DOM的異步更新,感受頗有意思,特地瞭解了一下。其中關於nextTick的源碼涉及到很多知識,nextTick 是 Vue 的一個核心實現,在介紹 Vue 的 nextTick 以前,爲了方便你們理解,我先簡單介紹一下 JS 的運行機制。html

JS 運行機制

JS 執行是單線程的,它是基於事件循環的。事件循環大體分爲如下幾個步驟:vue

(1)全部同步任務都在主線程上執行,造成一個執行棧(execution context stack)。react

(2)主線程以外,還存在一個"任務隊列"(task queue)。只要異步任務有了運行結果,就在"任務隊列"之中放置一個事件。ios

(3)一旦"執行棧"中的全部同步任務執行完畢,系統就會讀取"任務隊列",看看裏面有哪些事件。那些對應的異步任務,因而結束等待狀態,進入執行棧,開始執行。git

(4)主線程不斷重複上面的第三步。github

主線程的執行過程就是一個 tick,而全部的異步結果都是經過 「任務隊列」 來調度。 消息隊列中存放的是一個個的任務(task)。 規範中規定 task 分爲兩大類,分別是 macro taskmicro task,而且每一個 macro task 結束後,都要清空全部的 micro task。數組

關於 macro taskmicro task 的概念,這裏不會細講,簡單經過一段代碼演示他們的執行順序:promise

for (macroTask of macroTaskQueue) {
    // 1. Handle current MACRO-TASK
    handleMacroTask();
      
    // 2. Handle all MICRO-TASK
    for (microTask of microTaskQueue) {
        handleMicroTask(microTask);
    }
}
複製代碼

在瀏覽器環境中,常見的 macro tasksetTimeoutMessageChannelpostMessagesetImmediate;常見的 micro taskMutationObseverPromise.then瀏覽器

Vue 的實現(Vue 源碼 2.5+)

在 Vue 源碼 2.5+ 後,nextTick 的實現單獨有一個 JS 文件來維護它,它的源碼並很少,總共也就 100 多行。接下來咱們來看一下它的實現,在 src/core/util/next-tick.js 中:app

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

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]()
  }
}

// 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).
let microTimerFunc
let macroTimerFunc
let useMacroTask = false

// Determine (macro) task defer implementation.
// Technically setImmediate should be the ideal choice, but it's only available
// in IE. The only polyfill that consistently queues the callback after all DOM
// events triggered in the same loop is by using MessageChannel.
/* istanbul ignore if */
if (typeof setImmediate !== 'undefined' && isNative(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 {
  /* istanbul ignore next */
  macroTimerFunc = () => {
    setTimeout(flushCallbacks, 0)
  }
}

// Determine microtask defer implementation.
/* istanbul ignore next, $flow-disable-line */
if (typeof Promise !== 'undefined' && isNative(Promise)) {
  const p = Promise.resolve()
  microTimerFunc = () => {
    p.then(flushCallbacks)
    // in problematic UIWebViews, Promise.then doesn't completely break, but
    // it can get stuck in a weird state where callbacks are pushed into the
    // microtask queue but the queue isn't being flushed, until the browser
    // needs to do some other work, e.g. handle a timer. Therefore we can
    // "force" the microtask queue to be flushed by adding an empty timer.
    if (isIOS) setTimeout(noop)
  }
} else {
  // fallback to macro
  microTimerFunc = macroTimerFunc
}

/** * Wrap a function so that if any code inside triggers state change, * the changes are queued using a (macro) task instead of a microtask. */
export function withMacroTask (fn: Function): Function {
  return fn._withTask || (fn._withTask = function () {
    useMacroTask = true
    const res = fn.apply(null, arguments)
    useMacroTask = false
    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
    })
  }
}
複製代碼

next-tick.js 申明瞭 microTimerFuncmacroTimerFunc 2 個變量,它們分別對應的是micro task 的函數和 macro task 的函數。對於 macro task 的實現,優先檢測是否支持原生 setImmediate,這是一個高版本 IE 和 Edge 才支持的特性,不支持的話再去檢測是否支持原生的 MessageChannel,若是也不支持的話就會降級爲 setTimeout 0;而對於micro task的實現,則檢測瀏覽器是否原生支持 Promise,不支持的話直接指向 macro task 的實現。

next-tick.js 對外暴露了 2 個函數,先來看 nextTick,這就是咱們在上一節執行 nextTick(flushSchedulerQueue) 所用到的函數。它的邏輯也很簡單,把傳入的回調函數 cb 壓入 callbacks 數組,最後一次性地根據 useMacroTask 條件執行 macroTimerFunc 或者是 microTimerFunc,而它們都會在下一個 tick 執行 flushCallbacks,flushCallbacks 的邏輯很是簡單,對 callbacks 遍歷,而後執行相應的回調函數。

這裏使用 callbacks 而不是直接在 nextTick 中執行回調函數的緣由是保證在同一個 tick 內屢次執行 nextTick,不會開啓多個異步任務,而把這些異步任務都壓成一個同步任務,在下一個 tick 執行完畢。

nextTick 函數最後還有一段邏輯:

if (!cb && typeof Promise !== 'undefined') {
  return new Promise(resolve => {
    _resolve = resolve
  })
}
複製代碼

這是當 nextTick 不傳 cb 參數的時候,提供一個 Promise 化的調用,好比:

nextTick().then(() => {})
複製代碼

_resolve 函數執行,就會跳到 then 的邏輯中。

next-tick.js 還對外暴露了 withMacroTask 函數,它是對函數作一層包裝,確保函數執行過程當中對數據任意的修改,觸發變化執行 nextTick 的時候強制走 macroTimerFunc。好比對於一些 DOM 交互事件,如 v-on 綁定的事件回調函數的處理,會強制走 macro task

Vue.js 提供了 2 種調用 nextTick 的方式,一種是全局 API Vue.nextTick,一種是實例上的方法 vm.$nextTick,不管咱們使用哪種,最後都是調用 next-tick.js 中實現的 nextTick 方法。

再來看看vue2.5版本之前的源碼

/** * Defer a task to execute it asynchronously. */
export const nextTick = (function () {
  const callbacks = []
  let pending = false
  let timerFunc

  function nextTickHandler () {
    pending = false
    const copies = callbacks.slice(0)
    callbacks.length = 0
    for (let i = 0; i < copies.length; i++) {
      copies[i]()
    }
  }

  // the nextTick behavior leverages the microtask queue, which can be accessed
  // via either native Promise.then or MutationObserver.
  // MutationObserver has wider support, however it is seriously bugged in
  // UIWebView in iOS >= 9.3.3 when triggered in touch event handlers. It
  // completely stops working after triggering a few times... so, if native
  // Promise is available, we will use it:
  /* istanbul ignore if */
  if (typeof Promise !== 'undefined' && isNative(Promise)) {
    var p = Promise.resolve()
    var logError = err => { console.error(err) }
    timerFunc = () => {
      p.then(nextTickHandler).catch(logError)
      // in problematic UIWebViews, Promise.then doesn't completely break, but
      // it can get stuck in a weird state where callbacks are pushed into the
      // microtask queue but the queue isn't being flushed, until the browser
      // needs to do some other work, e.g. handle a timer. Therefore we can
      // "force" the microtask queue to be flushed by adding an empty timer.
      if (isIOS) setTimeout(noop)
    }
  } else if (!isIE && typeof MutationObserver !== 'undefined' && (
    isNative(MutationObserver) ||
    // PhantomJS and iOS 7.x
    MutationObserver.toString() === '[object MutationObserverConstructor]'
  )) {
    // use MutationObserver where native Promise is not available,
    // e.g. PhantomJS, iOS7, Android 4.4
    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)
    }
  } else {
    // fallback to setTimeout
    /* istanbul ignore next */
    timerFunc = () => {
      setTimeout(nextTickHandler, 0)
    }
  }

  return function queueNextTick (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()
    }
    if (!cb && typeof Promise !== 'undefined') {
      return new Promise((resolve, reject) => {
        _resolve = resolve
      })
    }
  }
})()

複製代碼

首先,先了解nextTick中定義的三個重要變量。

callbacks

用來存儲全部須要執行的回調函數

pending

用來標誌是否正在執行回調函數

timerFunc

用來觸發執行回調函數 接下來,瞭解nextTickHandler()函數。

function nextTickHandler () {
    pending = false
    const copies = callbacks.slice(0)
    callbacks.length = 0
    for (let i = 0; i < copies.length; i++) {
      copies[i]()
    }
}
複製代碼

這個函數用來執行callbacks裏存儲的全部回調函數。 接下來是將觸發方式賦值給timerFunc。

先判斷是否原生支持promise,若是支持,則利用promise來觸發執行回調函數; 不然,若是支持MutationObserver,則實例化一個觀察者對象,觀察文本節點發生變化時,觸發執行全部回調函數。 若是都不支持,則利用setTimeout設置延時爲0。

最後是queueNextTick函數。由於nextTick是一個即時函數,因此queueNextTick函數是返回的函數,接受用戶傳入的參數,用來往callbacks裏存入回調函數。

1

上圖是整個執行流程,關鍵在於timeFunc(),該函數起到延遲執行的做用。 從上面的介紹,能夠得知timeFunc()一共有三種實現方式。

Promise MutationObserver setTimeout

其中Promise和setTimeout很好理解,是一個異步任務,會在同步任務以及更新DOM的異步任務以後回調具體函數。 下面着重介紹一下MutationObserver。 MutationObserver是HTML5中的新API,是個用來監視DOM變更的接口。他能監聽一個DOM對象上發生的子節點刪除、屬性修改、文本內容修改等等。 調用過程很簡單,可是有點不太尋常:你須要先給他綁回調:

var mo = new MutationObserver(callback)
複製代碼

複製代碼經過給MutationObserver的構造函數傳入一個回調,能獲得一個MutationObserver實例,這個回調就會在MutationObserver實例監聽到變更時觸發。 這個時候你只是給MutationObserver實例綁定好了回調,他具體監聽哪一個DOM、監聽節點刪除仍是監聽屬性修改,尚未設置。而調用他的observer方法就能夠完成這一步:

var domTarget = 你想要監聽的dom節點
mo.observe(domTarget, {
      characterData: true //說明監聽文本內容的修改。
})
複製代碼

1

在nextTick中 MutationObserver的做用就如上圖所示。在監聽到DOM更新後,調用回調函數。 其實使用 MutationObserver的緣由就是 nextTick想要一個異步API,用來在當前的同步代碼執行完畢後,執行我想執行的異步回調,包括Promise和 setTimeout都是基於這個緣由。其中深刻還涉及到microtask等內容。

總結

@2.5後版本 $nextTick was using setImmediate > MessageChannel > setTimeout

@2.5前版本 $nextTick was using Promise > MutationObserver > setTimeout

經過這一節對 nextTick 的分析,並結合上一節的 setter 分析,咱們瞭解到數據的變化到 DOM 的從新渲染是一個異步過程,發生在下一個 tick。這就是咱們平時在開發的過程當中,好比從服務端接口去獲取數據的時候,數據作了修改,若是咱們的某些方法去依賴了數據修改後的 DOM 變化,咱們就必須在 nextTick 後執行。好比下面的僞代碼:

getData(res).then(()=>{
  this.xxx = res.data
  this.$nextTick(() => {
    // 這裏咱們能夠獲取變化後的 DOM
  })
})
複製代碼

參考

Vue.js 技術揭祕

簡單理解Vue中的nextTick

公衆號
相關文章
相關標籤/搜索