淺析setTimeout與Promise

關於JavaScript異步編程,前文解析過了JavaScript併發模型,該併發模型基於事件循環。正巧又在Stackoverflow上回答了一個關於setTimeout與Promise執行順序相關的問題,因而總結這一知識點,與更多讀者分享,同時完善JavaScript異步編程系列文章。javascript

個人我的博客前端

前言

咱們先看一到常見的前端面試題:java

var p1 = new Promise(function(resolve, reject){
    resolve(1);
})
setTimeout(function(){
  console.log("will be executed at the top of the next Event Loop");
},0)
p1.then(function(value){
  console.log("p1 fulfilled");
})
setTimeout(function(){
  console.log("will be executed at the bottom of the next Event Loop");
},0)
複製代碼

上例代碼執行輸出順序如何?這道題也是本文創做的源泉,其答案是:面試

p1 fulfilled
will be executed at the top of the next Event Loop
will be executed at the bottom of the next Event Loop
複製代碼

接下來展開解釋輸出結果緣由,看完本文應該能瞭解setTimeout和Promise的區別。編程

事件循環

事件循環相關詳細內容在JavaScript異步編程一文已經介紹過,本文再也不贅述,進行一些補充和總結:json

事件循環

可執行代碼

思考一下,JavaScript代碼是如何執行的呢?是一行一行代碼執行的嗎?固然不是,JavaScript 引擎一塊一塊地解析,執行JavaScript代碼,而非一行一行進行。在解析,執行代碼塊時,會須要有一個前期工做,如變量/函數提高,定義變量/函數。這裏所說的代碼塊,一般稱做可執行代碼(execuable code),一般包括全局代碼,函數代碼,eval執行代碼。而所作的前期工做就是建立執行上下文(execution context)。promise

執行上下文棧

每當JavaScript引擎開始執行應用程序時,都會建立一個執行上下文棧(後進先出),用以管理執行上下文。在執行一段可執行代碼時,會建立一個執行上下文,而後將其壓入棧,執行完畢便將該上下文退棧。併發

function funA() {
    console.log('funA')
}

function funB() {
    fun3A();
}

function funC() {
    funB();
}

funC();
複製代碼
ECStack.push(<funC> functionContext);

// funC中調用funB,需建立funB執行上下文,入棧
ECStack.push(<funB> functionContext);

// funB內調用funA,入棧上下文
ECStack.push(<funA> functionContext);

// funA執行完畢,退棧
ECStack.pop();

// funB執行完畢,退棧
ECStack.pop();

// funC執行完畢,退棧
ECStack.pop();

// javascript繼續執行後續代碼
複製代碼

另外,全部的代碼都是從全局環境開始執行,因此,必然棧底是全局執行上下文。異步

異步任務

回顧JavaScript事件循環併發模型,咱們瞭解了setTimeoutPromise調用的都是異步任務,這一點是它們共同之處,也即都是經過任務隊列進行管理/調度。那麼它們有什麼區別嗎?下文繼續介紹。async

任務隊列

前文已經介紹了任務隊列的基礎內容和機制,可選擇查看,本文對任務隊列進行拓展介紹。JavaScript經過任務隊列管理全部異步任務,而任務隊列還能夠細分爲MacroTask Queue和MicoTask Queue兩類。

MacroTask Queue

MacroTask Queue(宏任務隊列)主要包括setTimeout, setInterval, setImmediate, requestAnimationFrame, UI rendeing, NodeJS中的`I/O等。

MicroTask Queue

MicroTask Queue(微任務隊列)主要包括兩類:

  1. 獨立回調microTask:如Promise,其成功/失敗回調函數相互獨立;
  2. 複合回調microTask:如 Object.observe, MutationObserver 和NodeJs中的 process.nextTick ,不一樣狀態回調在同一函數體;

MacroTask和MicroTask

JavaScript將異步任務分爲MacroTask和MicroTask,那麼它們區別何在呢?

  1. 依次執行同步代碼直至執行完畢;
  2. 檢查MacroTask 隊列,如有觸發的異步任務,則取第一個並調用其事件處理函數,而後跳至第三步,若沒有需處理的異步任務,則直接跳至第三步;
  3. 檢查MicroTask隊列,而後執行全部已觸發的異步任務,依次執行事件處理函數,直至執行完畢,而後跳至第二步,若沒有需處理的異步任務中,則直接返回第二步,依次執行後續步驟;
  4. 最後返回第二步,繼續檢查MacroTask隊列,依次執行後續步驟;
  5. 如此往復,若全部異步任務處理完成,則結束;

task Queue

The microTask queue is processed after callbacks as long as no other JavaScript is mid-execution, and at the end of each task. 只要沒有其餘JavaScript代碼在執行,而且在每一個任務結束時,就會開始處理microTask隊列。

須要注意的是,此處說的的每一個任務結束時中的任務一般就是指macroTask,有一個比較特殊的任務- 腳本執行(JavaScript Run,也是一個macroTask,會在JavaScript腳本執行時,當即將JavaScript Run任務入棧macroTask隊列。

回顧

本文內容介紹基本結束,那麼前文第一個題目輸出順序是爲何呢?簡單解釋一下:

  1. 開始執行JavaScript腳本,將任務JavaScript Run入棧macroTask隊列;
  2. 同步resolvePromise後;
  3. 入棧第一個setTimeout任務進入macroTask隊列
  4. 入棧Proimse.then任務進入microTask隊列;
  5. 入棧第二個setTimeout任務進入macroTask隊列;
  6. 同步執行代碼完畢,退出第一個macroTask,即JavaScript Run;
  7. 執行清空microTask;
  8. 執行下一個macroTask;

最後,咱們以一個題目再次回顧一下內容:

setTimeout(function(){
  console.log("will be executed at the top of the next Event Loop")
},0)
var p1 = new Promise(function(resolve, reject){
    setTimeout(() => { resolve(1); }, 0);
});
setTimeout(function(){
    console.log("will be executed at the bottom of the next Event Loop")
},0)
for (var i = 0; i < 100; i++) {
    (function(j){
        p1.then(function(value){
           console.log("promise then - " + j)
        });
    })(i)
}

複製代碼

代碼輸出結果是什麼呢?快點確認一下吧:

will be executed at the top of the next Event Loop
promise then - 0
promise then - 1
promise then - 2
...
promise then - 99
will be executed at the bottom of the next Event Loop
複製代碼
  1. 首先同步執行完全部代碼,其間註冊了三個setTimeout異步任務,100個Promise異步任務;
  2. 而後檢查MacroTask隊列,取第一個到期的MacroTask,執行輸出will be executed at the top of the next Event Loop;
  3. 而後檢查MicroTask隊列,發現沒有到期的MicroTask,進入第4步;
  4. 再次檢查MacroTask,執行第二個setTimeout處理函數,resolve Promise;
  5. 而後檢查MicroTask隊列,發現Promise已解決,其異步處理函數都可執行,依次執行,輸出promise then - 0promise then - 99
  6. 最後再次檢查MacroTask隊列,執行輸出will be executed at the bottom of the next Event Loop
  7. 交替往復檢查兩個異步任務隊列,直至執行完畢;
相關文章
相關標籤/搜索