Vue nextTick實現原理


前言

熟悉 vue 的前端,想必對 vue 里的 nextTick 也很熟悉了,用的時候就知道他是延遲回調,有時候用起來甚至和setTimeout 看起來是同樣的效果。但他和setTimeout到底有什么區別?他是如何實現的?
本文就nextTick的實現引入,來探討下js中的異步與同步,微任務與宏任務。

nextTick

用法

先搬運下文檔 Vue-nextTick

在下次 DOM 更新循環結束之后執行延遲回調。在修改數據之后立即使用這個方法,獲取更新后的 DOM

// 修改數據
vm.msg = 'Hello'
// DOM 還沒有更新
Vue.nextTick(function () {
  // DOM 更新了
})

// 作為一個 Promise 使用 (2.1.0 起新增,詳見接下來的提示)
Vue.nextTick()
  .then(function () {
    // DOM 更新了
  })

源碼實現

在了解原理之前先看下 nextTick 源碼實現

// 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 next, $flow-disable-line */
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
} 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
} 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)
  }
}

可以看到上面有幾個條件判斷 如果支持 Promise 就用 Promise
如果不支持就用 MutationObserver MDN-MutationObserver
MutationObserver 它會在指定的DOM發生變化時被調用
如果不支持 MutationObserver 的話就用 setImmediate MDN-setImmediate
但是這個特性只有最新版IE和node支持,然后是最后一個條件 如果這些都不支持的話就用setTimeout。
看完這一段其實也很懵,為什么要這樣設計呢?為什么要這樣一個順序來判斷呢?說到這里就不得不討論JavaScript 運行機制(Event Loop)&微任務宏任務了。

JavaScript 運行機制(Event Loop)

單線程

JS是單線程,同一個時間只能做一件事。至於JS為什么是單線程?

JavaScript的單線程,與它的用途有關。作為瀏覽器腳本語言,JavaScript的主要用途是與用戶互動,以及操作DOM。這決定了它只能是單線程,否則會帶來很復雜的同步問題。比如,假定JavaScript同時有兩個線程,一個線程在某個DOM節點上添加內容,另一個線程刪除了這個節點,這時瀏覽器應該以哪個線程為准?所以,為了避免復雜性,從一誕生,JavaScript就是單線程,這已經成了這門語言的核心特征,將來也不會改變。

同步和異步

js里的任務分為兩種:同步任務(synchronous)和異步任務(asynchronous)。同步阻塞異步非阻塞。
同步任務指的是,在主線程上排隊執行的任務,只有前一個任務執行完畢,才能執行后一個任務,例如alert,會阻塞后續任務的執行,只有在點擊確定之后,才會執行下一個任務。
異步任務指的是,不進入主線程、而進入"任務隊列"(task queue)的任務,只有"任務隊列"通知主線程,某個異步任務可以執行了,該任務才會進入主線程執行。
單線程就意味着,所有任務需要排隊,前一個任務結束,才會執行后一個任務。所以會有任務隊列的概念。正因為是單線程,所以所有任務都是主線程執行的,異步請求這些也不會開辟新的線程,而是放到任務隊列,當這些異步操作被觸發時才進入主線程執行。

宏任務和微任務

JS任務又分為宏任務和微任務。
宏任務(macrotask):setTimeout、setInterval、setImmediate、I/O、UI rendering
微任務(microtask):promise.then、process.nextTick、MutationObserver、queneMicrotask(開啟一個微任務)

宏任務按順序執行,且瀏覽器在每個宏任務之間渲染頁面
瀏覽器為了能夠使得JS內部task與DOM任務能夠有序的執行,會在一個task執行結束后,在下一個 task 執行開始前,對頁面進行重新渲染 (task->渲染->task->...)

微任務通常來說就是需要在當前 task 執行結束后立即執行的任務,比如對一系列動作做出反饋,或或者是需要異步的執行任務而又不需要分配一個新的 task,這樣便可以減小一點性能的開銷。只要執行棧中沒有其他的js代碼正在執行且每個宏任務執行完,微任務隊列會立即執行。如果在微任務執行期間微任務隊列加入了新的微任務,會將新的微任務加入隊列尾部,之后也會被執行。

何時使用微任務

微任務的執行時機,晚於當前本輪事件循環的 Call Stack(調用棧)中的代碼(宏任務),遭遇事件處理函數和定時器的回調函數

使用微任務的原因

減少操作中用戶可感知到的延遲
確保任務順序的一致性,即便當結果或數據是同步可用的
批量操作的優化

了解了宏任務和微任務的執行順序,就可以了解到為何nextTick 要優先使用PromiseMutationObserver 因為他倆屬於微任務,會在執行棧空閑的時候立即執行,它的響應速度相比setTimeout會更快,因為無需等渲染。
而setImmediate和setTimeout屬於宏任務,執行開始之前要等渲染,即task->渲染->task。

參考:
阮一峰-JavaScript 運行機制詳解:再談Event Loop
譯文:JS事件循環機制(event loop)之宏任務、微任務
Generator 函數的含義與用法
瀏覽器進程、JS事件循環機制、宏任務和微任務


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM