理解 JavaScript 中的 macrotask 和 microtask

前言

在實現 Promise/A+ 庫的過程當中,第一次據說了 JavaScript 中的 macrotask 和 microtask 的概念。而後 Google 搜索到瞭如下的資料:html

閱讀以後結合我本身的理解,來講說這二者的區別。node

異步任務運行機制

阮一峯 - JavaScript 運行機制詳解:再談Event Loopgit

Philip Roberts - Help,I’m stuck in an event loopgithub

在那以前,咱們先說一說 JavaScript 中的事件循環機制,上面兩個連接中的文章和視頻很是詳細的解釋了該機制。咱們只簡單的解釋一下,先運行下面的一段代碼:web

console.log('script start');
setTimeout(function() {
  console.log('setTimeout');
}, 0);
console.log('script end');

這裏一看,setTimeout的延時爲 0 ,那麼是否是程序執行到這裏以後就當即執行setTimeout裏面的函數呢?其實不是的,此段代碼最後的輸出結果爲:api

"script start"
"script end"
"setTimeout"

 

這是由於 JavaScript 主線程擁有一個 執行棧 以及一個 任務隊列,主線程會依次執行代碼,當遇到函數時,會先將函數 入棧,函數運行完畢後再將該函數 出棧,直到全部代碼執行完畢。promise

那麼遇到 WebAPI(例如:setTimeoutAJAX)這些函數時,這些函數會當即返回一個值,從而讓主線程不會在此處阻塞。而真正的異步操做會由瀏覽器執行,瀏覽器會在這些任務完成後,將事先定義的回調函數推入主線程的 任務隊列 中。瀏覽器

而主線程則會在 清空當前執行棧後,按照先入先出的順序讀取任務隊列裏面的任務。app

那麼咱們來看一下上面程序的執行順序:webapp

// 1. 開始執行
console.log('script start'); // 2. 打印字符串 "script start"
setTimeout(
    function() {                 // 5. 瀏覽器在 0ms 以後將該函數推入任務隊列
                                 //    而到第5步時纔會被主線程執行
      console.log('setTimeout'); // 6. 打印字符串 "setTimeout"
    },
    0
);                       // 3. 調用 setTimeout 函數,並定義其完成後執行的回調函數
console.log('script end');   // 4. 打印字符串 "script end"
// 5. 主線程執行棧清空,開始讀取 任務隊列 中的任務

 

以上就是瀏覽器的異步任務的執行機制,核心點爲:

  • 異步任務是由瀏覽器執行的,不論是AJAX請求,仍是setTimeout等 API,瀏覽器內核會在其它線程中執行這些操做,當操做完成後,將操做結果以及事先定義的回調函數放入 JavaScript 主線程的任務隊列中
  • JavaScript 主線程會在執行棧清空後,讀取任務隊列,讀取到任務隊列中的函數後,將該函數入棧,一直運行直到執行棧清空,再次去讀取任務隊列,不斷循環
  • 當主線程阻塞時,任務隊列仍然是可以被推入任務的。這也就是爲何當頁面的 JavaScript 進程阻塞時,咱們觸發的點擊等事件,會在進程恢復後依次執行。

Macrotasks 和 Microtasks

基本介紹

Macrotask 和 microtask 都是屬於上述的異步任務中的一種,咱們先看一下他們分別是哪些 API :

  • macrotasks: setTimeoutsetIntervalsetImmediate, I/O, UI rendering
  • microtasks: process.nextTickPromisesObject.observe(廢棄), MutationObserver

    setTimeout 的 macrotask ,和 Promise 的 microtask 有什麼不一樣呢? 咱們經過下面的代碼來展示他們的不一樣點:

console.log('script start');
setTimeout(function() {
  console.log('setTimeout');
}, 0);
Promise.resolve().then(function() {
  console.log('promise1');
}).then(function() {
  console.log('promise2');
});
console.log('script end');

(代碼來自 Tasks, microtasks, queues and schedules,原文有執行順序的可視化操做演示,推薦觀看)

在這裏,setTimeout的延時爲0,而Promise.resolve()也是返回一個被resolvepromise對象,即這裏的then方法中的函數也是至關於異步的當即執行任務,那麼他們究竟是誰在前誰在後?

咱們看看最終的運行結果(node 7.7.3):

"script start"
"script end"
"promise1"
"promise2"
"setTimeout"

 

這裏的運行結果是Promise的當即返回的異步任務會優先於setTimeout延時爲0的任務執行。

緣由是任務隊列分爲 macrotasks 和 microtasks,而Promise中的then方法的函數會被推入 microtasks 隊列,而setTimeout的任務會被推入 macrotasks 隊列。在每一次事件循環中,macrotask 只會提取一個執行,而 microtask 會一直提取,直到 microtasks 隊列清空。

注:通常狀況下,macrotask queues 咱們會直接稱爲 task queues,只有 microtask queues 纔會特別指明。

那麼也就是說若是個人某個 microtask 任務又推入了一個任務進入 microtasks 隊列,那麼在主線程完成該任務以後,仍然會繼續運行 microtasks 任務直到任務隊列耗盡。

而事件循環每次只會入棧一個 macrotask ,主線程執行完該任務後又會先檢查 microtasks 隊列並完成裏面的全部任務後再執行 macrotask

事件循環進程模型

如今咱們根據 HTML Standard - event loop processing model來描述瀏覽器的事件循環的進程模型:

  1. 選擇最早進入 事件循環任務隊列的一個任務, 若是隊列中沒有任務,則直接跳到第6步的 Microtask
  2. 設置 事件循環的當前運行任務爲上一步所選擇的任務
  3. Run: 運行所選任務
  4. 設置 事件循環的當前運行任務爲 null
  5. 將剛剛第3步運行的任務從它的任務隊列中刪除
  6. Microtasksperform a microtask checkpoint
  7. 更新並渲染界面
  8. 返回第1步

perform a microtask checkpoint 的執行步驟:

  1. 設置 performing a microtask checkpoint 的標記爲 true
  2. Microtask queue handling: 若是事件循環的 microtask queue 是空,跳到第8步 Done
  3. 選取最早進入 microtask queue 的 microtask
  4. 設置 事件循環的當前運行任務 爲上一步所選擇的任務
  5. Run: 執行所選取的任務
  6. 設置 事件循環的當前運行任務 爲 null
  7. 將剛剛第5步運行的 microtask 從它的 microtask queue 中刪除
  8. Done: For each environment settings object whose responsible event loop is this event loop, notify about rejected promises on that environment settings object (此處建議查看原網頁)
  9. 清理 Index Database 的事務
  10. 使 performing a microtask checkpoint 的標記爲 false

在咱們的瀏覽器環境的事件循環中, JavaScript 腳本也會做爲一個 task 被推入 task queue,咱們在運行這個事件後,該腳本中的 microtasks,tasks 纔會被推入隊列。

Microtask 的應用

Vue 中如何使用 MutationObserver 作批量處理 - 顧軼靈的回答

爲啥要用 microtask?根據 HTML Standard,在每一個 task 運行完之後,UI 都會重渲染,那麼在 microtask 中就完成數據更新,當前 task 結束就能夠獲得最新的 UI 了。反之若是新建一個 task 來作數據更新,那麼渲染就會進行兩次。

根據咱們上面提到的事件循環進程模型,每一次執行 task 後,而後執行 microtasks queue,最後進行頁面更新。若是咱們使用 task 來設置 DOM 更新,那麼效率會更低。而 microtask 則會在頁面更新以前完成數據更新,會獲得更高的效率。

Microtask 的實現

immediate 庫是一個跨瀏覽器的 microtask 實現。

這個庫使用原生 JavaScript 實現了可以兼容 IE6 的 microtask ,若是對實現機制比較感興趣的能夠去閱讀這個庫的源碼,我後面會寫一篇文章來詳細的介紹一下其實現。

 

原文連接:https://juejin.im/entry/58d4df3b5c497d0057eb99ff

相關文章
相關標籤/搜索