從 Event Loop 角度解讀 Vue NextTick 源碼

解讀背景

  1. 在學習 vue 源碼,nextTick 方法藉助了瀏覽器的 event loop 事件循環做到了異步更新。

  2. 在公司面試的時候,筆試題最喜歡出關於 JavaScript 運行機制,Promise/A+ 等關於 event loop 線程的題目。

  3. 學會 nextTick 原理幫助定位 BUG , 使用 Vue 會更加靈活。

什麼是 event loop

先看一張圖 (來自 mr.z 大佬)

  1. 先執行同步阻塞任務,同步任務會等待上一個執行完畢以後執行下一個,當同步任務執行完畢,再執行異步任務,遇到異步任務會將異步任務的回調函數註冊在異步任務隊列裏。注意,如果主線程上沒有同步任務會直接調用異步任務的微任務。

  2. 執行宏任務,遇到微任務將都添加到微任務隊列裏。

  3. 開始執行微任務隊列,當宏任務執行完後執行微任務隊列,直到微任務隊列全部執行完,微任務隊列爲空。

  4. 執行宏任務,如果在執行宏任務期間有微任務,將微任務添加到微任務隊列裏,執行完宏任務之後執行微任務,直到微任務隊列全部執行完。

  5. 繼續執行宏任務隊列。

重複 2, 3, 4,5…… 直到宏微任務爲空。

$nextTick 的實現原理

從字面意思理解,next 下一個,tick 滴答(鐘錶)來源於定時器的週期性中斷(輸出脈衝),一次中斷表示一個 tick,也被稱做一個 “時鐘滴答”),nextTick 顧名思義就是下一個時鐘滴答。看源碼,在 Vue 2.x 版本中,nextTick 在 src\core\util 中的一個單獨的文件 next-tick.js ,可見 nextTick 的重要性,雖然短短 200 多行,尤大卻單獨創建一個文件去維護。

接下來我們來看整個文件。

  1. 聲明瞭三個全局變量,callbacks: [] ,pending: Boolean,timerFunc: undefined

  2. 聲明瞭一個函數 flushCallbacks

  3. 一堆 **if,else if ** 判斷。

  4. 拋出了一個函數 nextTick

nextTick 函數

  1. 聲明一個局部變量 _resolve 。

  2. 把所有回調函數壓進 callbacks 中,以棧的形式的存儲所有 callback

  3. 當 pending 爲 false 時,執行 timerFunc 函數。

  4. 當沒有 callback 的時候,返回一個 Promise 的調用方式,可以用 .then 接收。

timerFunc 函數

我們開始說了,timerFunc 爲全局變量,現在調用 timerFunc ,timerFunc 是什麼時候被賦值爲一個函數,並且函數里執行代碼又是什麼?

我們看到,這段判斷代碼總共有四個分支,四個分支裏對 timerFunc 有不同的賦值,我們先來看第一個分支。

Promise 分支

if (typeof Promise !== 'undefined' && isNative(Promise)) {
  const p = Promise.resolve()
  timerFunc = () ={
    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)
  }
  isUsingMicroTask = true
}
複製代碼
  1. 判斷環境是否支持 Promise 並且 Promise 是否爲原生。

  2. 使用 Promise 異步調用 flushCallbacks 函數。

  3. 當執行環境是 iPhone 等,使用 setTimeout 異步調用 noop ,iOS 中在一些異常的 webview 中,promise 結束後任務隊列並沒有刷新所以強制執行 setTimeout 刷新任務隊列。

MutationObserver 分支

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
  // (#6466 MutationObserver is unreliable in IE11)
  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
}
複製代碼
  1. 對非 IE 瀏覽器和是否可以使用 HTML5 新特性 MutationObserver 進行判斷。

  2. 實例一個 MutationObserver 對象,這個對象主要是對瀏覽器 DOM 變化進行監聽,當實例化 MutationObserver 對象並且執行對象 observe,設置 DOM 節點發生改變時自動觸發回調。

  3. 把 timerFunc 賦值爲一個改變 DOM 節點的方法,當 DOM 節點發生改變,觸發 flushCallbacks 。(這裏其實就是想用利用 MutationObserver 的特性進行異步操作)

setImmediate 分支

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)
  }
}
複製代碼
  1. 判斷 setImmediate 是否存在,setImmediate 是高版本 IE (IE10+) 和 edge 才支持的。

  2. 如果存在,傳入 flushCallbacks 執行 setImmediate 。

setTimeout 分支

else {
  // Fallback to setTimeout.
  timerFunc = () ={
    setTimeout(flushCallbacks, 0)
  }
}
複製代碼
  1. 當以上所有分支異步 api 都不支持的時候,使用 macro task (宏任務)的 setTimeout 執行 flushCallbacks 。

執行降級

我們可以發現,給 timerFunc 賦值是一個降級的過程。爲什麼呢,因爲 Vue 在執行的過程中,執行環境不同,所以要適配環境。

這張圖便於我們更清晰的瞭解到降級的過程。

flushCallbacks 函數

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

循環遍歷,按照 隊列 數據結構 “先進先出” 的原則,逐一執行所有 callback 。

總結

到這裏就全部講完了,nextTick 的原理就是利用 Event loop 事件線程去異步重新渲染,分支判斷首要選擇 Promise 的原因是當同步 JS 代碼執行完畢,執行棧清空會首先查看 micro task (微任務)隊列是否爲空,不爲空首先執行微任務。在我們 DOM 依賴數據發生變化的時候,會異步重新渲染 DOM ,但是比如像 echarts ,canvas…… 這些 Vue 無法在初始狀態下收集依賴的 DOM ,我們就需要手動執行 nextTick 方法使其重新渲染。

本文由 Readfog 進行 AMP 轉碼,版權歸原作者所有。
來源https://mp.weixin.qq.com/s/RoNIr8pawc8A_vOtAHVDcQ