1. 程式人生 > 程式設計 >JS promise 的回撥和 setTimeout 的回撥到底誰先執行

JS promise 的回撥和 setTimeout 的回撥到底誰先執行

目錄
  • 任務 VS 微任務
  • 執行過程
  • 案例分析
  • 結語 & 參考資料

首先提一個小問題:執行下面這段 程式碼後控制檯的輸出是什麼?

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

怎麼樣,你猜對了嗎?如果對這個輸出結果感到很迷惑,這篇文章或許可以幫到你。

PS:文中按照標準分析理論結果,但實際上各個瀏覽器對任務佇列的支援情況很混亂,所以如果你在瀏覽器執行程式碼後發現結果不同也不必糾結;總體來說 Chrome 的支援比較好。

如果對 Promise 的用法還不熟悉,可以看我的上一篇部落格:前端 | JS Promise:axios 請求結果後面的 .then() 是什麼意思?

任務 VS 微任務

設計的本質是單執行緒語言,但隨著硬體效能的飛速發展,純單執行緒已經不太能夠滿足需求了。因此 JS 逐漸發展出了任務和微任務,來模擬實現多執行緒。

瀏覽器中,對於每個(有時也可能是多個同源網頁),網頁的程式碼和瀏覽器自身的使用者介面程式運共享同一個主執行緒,它除了執行瀏覽器交給它的 JS 程式碼,也負責收集和派發事件、渲染和繪製網頁內容等等。因此,如果主執行緒中的某個任務阻塞了,其他任務都會受到影響;這就是為什麼有時候網頁程式碼出現了錯誤會導致整個網頁渲染失敗。

每個主執行緒都由一個事件迴圈 Event loops驅動。事件迴圈可以理解為一個任務佇列,JS 引擎不斷的進行“迴圈-等待”,按順序處理佇列中的任務。事件迴圈中的任務稱作“

任務 Task”,由宿主環境(瀏覽器)建立;每個任務都是宿主計劃執行的 Script 程式碼,如程式初始化、解析HTML、事件觸發的回撥(例如點選網頁上的按鈕),或是由setTimeout()setInterval()等 API 新增的回撥函式。

JS 引擎在執行一個任務的過程中,有時會進行一些非同步操作,不會立即執行,但又想在同一個任務中完成、不留到事件迴圈中的下一個任務裡;例如常用的 promise、監控 DOM 的回撥等。這時,JS 引擎會建立一個“微任務 Mircotask”,並加入當前的微任務佇列中。(有時為了區分,也把任務task稱為“巨集任務”。)

事件迴圈、任務、微任務的示意圖如下:

JS promise 的回撥和 setTimeout 的回撥到底誰先執行

執行過程

一個主執行緒的執行過程如下:

  • 拿出事件迴圈中的下一個任務
  • 執行任務本身的 Script 程式碼;期間可能會往任務佇列、微任務佇列建立新增新任務
  • script 執行完後,檢查微任務佇列
    • 如果有微任務,順序執行,期間可能還會建立新的任務和微任務
    • 如果微任務佇列為空,這個任務執行結束,回到第一步

可以看出,在一個任務中會反覆檢查微任務佇列,直到沒有微任務存在了才會執行下一個任務。因此在任務和微任務指令碼中建立的所有微任務都會在這個任務結束前執行,同時也意味著會早於其他所有建立的任務執行(因為新建的任務都加入了任務佇列)。

JS promise 的回撥和 setTimeout 的回撥到底誰先執行

案例分析

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

console.log("script start");

setTimeout(function () {
  console.log("setTimeout1");
},reject) => {
  setTimeout(function sdKUnf() {
    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 startscript end
  • 任務佇列中(除當前任務以外)有2個任務(兩個setTimeout()的回撥按時間先後順序排列)
  • 微任務佇列中有1個任務(promise 的回撥)

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

  • console.log("promise2")輸出
  • 隱式 return,相當於返回一個Promise.resolve(undefined);因此 Promise 鏈中的下一個 then 回撥被加入微任務佇列
  • 微任務執行完畢

此時:

  • 控制檯輸出了script startscript endpromise2
  • 任務佇列中(除當前任務以外)有2個任務(兩個setTimeout()的回撥按時間先後順序排列)
  • 微任務佇列中有1個任務(第二個 promise 回撥)

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

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

此時:

  • 控制檯輸出了script startscript endpromise2promise3
  • 任務佇列中(除當前任務以外)有2個任務(兩個setTimeout()的回撥按時間先後順序排列)
  • 微任務佇列為空

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

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

此時:

  • 控制檯輸出了script startscript endpromise2promise3setTimeout1
  • 任務佇列中(除當前任務以外)有1個任務
  • 微任務佇列為空

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

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

此時:

  • 控制檯輸出了script startscript endpromise2promise3setTimeout1swww.cppcns.cometTimeout2
  • 任務佇列中只有當前任務
  • 微任務佇列中有一個任務(promise 的回撥)

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

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

此時:

  • 控制檯輸出了script startscript endpromise2promise3setTimeout1setTimeout2promise1
  • 任務佇列中只有當前任務
  • 微任務佇列為空

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

結語 & 參考資料

非同步操作已經是平時開發過程中不可避免經常會遇到的用法了,平時都是馬馬虎虎的用,最近終於認真學習了一下,感覺頗有收穫。不過話說回來,理論學習和實際開發畢竟存在差異。首先各種瀏覽器的支援只能說是慘不忍睹,所以真實開發過程中不能太過依賴理論分析的結果,需要實際測試程式碼功能的相容性;另一方面,過於複雜的巢狀非同步操作,容易造成沒必要的錯誤,同時導致程式碼很難理解和維護,能不用最好不用,KISS。

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

Tasks,microtasks,queues and schedules

深入:微任務與Javascript執行時環境

到此這篇關於JS promise 的回撥和 setTimeout 的回撥到底誰先執行 的文章就介紹到這了,更多相關JS promise 的回撥和 setTimeout 的回撥執行 內容請搜尋我們以前的文章或繼續瀏覽下面的相關文章希望大家以後多多支援我們!