前端 | JS 任務和微任務:promise 的回調和 setTimeout 的回調到底誰先執行?

首先提一個小問題:運行下面這段 JS 代碼後控制檯的輸出是什麼?html

console.log("script start");

setTimeout(function () {
  console.log("setTimeout1");
}, 0);

new Promise((resolve, reject) => {
  setTimeout(function () {
    console.log("setTimeout2");
    resolve();
  }, 100);
}).then(function () {
  console.log("promise1");
});

Promise.resolve()
  .then(function () {
    console.log("promise2");
  })
  .then(function () {
    console.log("promise3");
  });

console.log("script end");

能夠先嚐試本身分析一下結果,而後再看答案:前端

script start
script end
promise2
promise3
setTimeout1
setTimeout2
promise1

怎麼樣,你猜對了嗎?若是對這個輸出結果感到很迷惑,這篇文章或許能夠幫到你。ios

PS:文中按照標準分析理論結果,但實際上各個瀏覽器對任務隊列的支持狀況很混亂,因此若是你在瀏覽器執行代碼後發現結果不一樣也沒必要糾結;整體來講 Chrome 的支持比較好。axios

若是對 Promise 的用法還不熟悉,能夠看個人上一篇博客:前端 | JS Promise:axios 請求結果後面的 .then() 是什麼意思?promise

任務 VS 微任務

JavaScript 設計的本質是單線程語言,但隨着硬件性能的飛速發展,純單線程已經不太可以知足需求了。所以 JS 逐漸發展出了任務和微任務,來模擬實現多線程。瀏覽器

瀏覽器中,對於每一個網頁(有時也多是多個同源網頁),網頁的代碼和瀏覽器自身的用戶界面程序運共享同一個主線程,它除了運行瀏覽器交給它的 JS 代碼,也負責收集和派發事件、渲染和繪製網頁內容等等。所以,若是主線程中的某個任務阻塞了,其餘任務都會受到影響;這就是爲何有時候網頁代碼出現了錯誤會致使整個網頁渲染失敗。多線程

每一個主線程都由一個事件循環 Event loops 驅動。事件循環能夠理解爲一個任務隊列,JS 引擎不斷的進行「循環-等待」,按順序處理隊列中的任務。事件循環中的任務稱做「任務 Task」,由宿主環境(瀏覽器)建立;每一個任務都是宿主計劃執行的 JavaScript 代碼,如程序初始化、解析HTML、事件觸發的回調(例如點擊網頁上的按鈕),或是由 setTimeout() setInterval() 等 API 添加的回調函數。異步

JS 引擎在執行一個任務的過程當中,有時會進行一些異步操做,不會當即執行,但又想在同一個任務中完成、不留到事件循環中的下一個任務裏;例如經常使用的 promise、監控 DOM 的回調等。這時,JS 引擎會建立一個「微任務 Mircotask」,並加入當前的微任務隊列中。(有時爲了區分,也把任務task稱爲「宏任務」。)ide

事件循環、任務、微任務的示意圖以下:函數

未命名文件

執行過程

一個主線程的執行過程以下:

  1. 拿出事件循環中的下一個任務
  2. 執行任務自己的 Script 代碼;期間可能會往任務隊列、微任務隊列建立添加新任務
  3. script 執行完後,檢查微任務隊列
    • 若是有微任務,順序執行,期間可能還會建立新的任務和微任務
    • 若是微任務隊列爲空,這個任務執行結束,回到第一步

能夠看出,在一個任務中會反覆檢查微任務隊列,直到沒有微任務存在了纔會執行下一個任務。所以在任務腳本和微任務腳本中建立的全部微任務都會在這個任務結束前執行,同時也意味着會早於其餘全部建立的任務執行(由於新建的任務都加入了任務隊列)。

![未命名文件 (https://i.loli.net/2021/04/03/ZBNdi56S4PcYekw.png)](../../../Download/未命名文件 (1).jpg)

案例分析

明白了任務和微任務的區別,下面再來看文章開頭的例子:

console.log("script start");

setTimeout(function () {
  console.log("setTimeout1");
}, 0);

new Promise((resolve, reject) => {
  setTimeout(function () {
    console.log("setTimeout2");
    resolve();
  }, 100);
}).then(function () {
  console.log("promise1");
});

Promise.resolve()
  .then(function () {
    console.log("promise2");
  })
  .then(function () {
    console.log("promise3");
  });

console.log("script end");

接下來逐步跟蹤代碼的執行過程;若是感受文字不夠直觀,能夠看這篇博客中給出的逐步執行動畫。

整個 Script 會被宿主環境傳給 JS 引擎,做爲任務隊列中的一個任務;首先執行任務中的腳本代碼:

  • line1: console.log("script start") 是同步代碼,直接輸出
  • line3: 執行 setTimeout(),在0秒後將 console.log("setTimeout1"); 加入任務隊列
  • line8: 執行 setTimeout(),在0.1秒後將 console.log("setTimeout2");resolve() 加入任務隊列
  • line16: 返回一個已成功的 promise,第一個 then 回調被加入微任務隊列
  • line24: console.log("script end") 是同步代碼,直接輸出
  • 任務 script 執行完畢

此時:

  • 控制檯輸出了 script start script end
  • 任務隊列中(除當前任務之外)有2個任務(兩個 setTimeout() 的回調按時間前後順序排列)
  • 微任務隊列中有1個任務(promise 的回調)

接下來檢查微任務隊列,執行隊首的微任務:

  • console.log("promise2") 輸出
  • 隱式 return,至關於返回一個 Promise.resolve(undefined);所以 Promise 鏈中的下一個 then 回調被加入微任務隊列
  • 微任務執行完畢

此時:

  • 控制檯輸出了 script start script end promise2
  • 任務隊列中(除當前任務之外)有2個任務(兩個 setTimeout() 的回調按時間前後順序排列)
  • 微任務隊列中有1個任務(第二個 promise 回調)

再次檢查微任務隊列,執行隊首的微任務:

  • console.log("promise3") 輸出
  • 隱式 return(但此時 Promise 鏈已經結束了,因此無事發生)
  • 微任務執行完畢

此時:

  • 控制檯輸出了 script start script end promise2 promise3
  • 任務隊列中(除當前任務之外)有2個任務(兩個 setTimeout() 的回調按時間前後順序排列)
  • 微任務隊列爲空

檢查微任務隊列,發現沒有微任務了,當前任務結束;開始執行任務隊列中的下一個任務(0秒後執行的回調):

  • console.log("setTimeout1"); 輸出
  • 任務 script 執行完畢

此時:

  • 控制檯輸出了 script start script end promise2 promise3 setTimeout1
  • 任務隊列中(除當前任務之外)有1個任務
  • 微任務隊列爲空

檢查微任務隊列,發現沒有微任務,當前任務結束;開始執行任務隊列中的下一個任務(0.1秒後執行的回調):

  • console.log("setTimeout2"); 輸出
  • resolve(); 將 promise 的狀態更改成已成功;then 回調被加入微任務隊列
  • 任務 script 執行完畢

此時:

  • 控制檯輸出了 script start script end promise2 promise3 setTimeout1 setTimeout2
  • 任務隊列中只有當前任務
  • 微任務隊列中有一個任務(promise 的回調)

檢查微任務隊列,執行隊首的微任務:

  • console.log("promise1") 輸出
  • 隱式 return(但此時 Promise 鏈已經結束了,因此無事發生)
  • 微任務執行完畢

此時:

  • 控制檯輸出了 script start script end promise2 promise3 setTimeout1 setTimeout2 promise1
  • 任務隊列中只有當前任務
  • 微任務隊列爲空

檢查微任務隊列,發現沒有微任務,當前任務結束。任務隊列中沒有其餘任務,執行完畢。

結語 & 參考資料

異步操做已是平時開發過程當中不可避免常常會遇到的用法了,平時都是馬馬虎虎的用,最近終於認真學習了一下,感受很有收穫。不過話說回來,理論學習和實際開發畢竟存在差別。首先各類瀏覽器的支持只能說是慘不忍睹,因此真實開發過程當中不能太過依賴理論分析的結果,須要實際測試代碼功能的兼容性;另外一方面,過於複雜的嵌套異步操做,容易形成不必的錯誤,同時致使代碼很難理解和維護,能不用最好不用,KISS。

以上是我的學習JS的任務/微任務機制時的一些思考和總結,但願能對你有所幫助;文中可能存在疏漏和錯誤,敬請討論和指正。

Tasks, microtasks, queues and schedules

深刻:微任務與Javascript運行時環境

相關文章
相關標籤/搜索