從promise、process.nextTick、setTimeout出發,談談Event Loop中的Job queue

嗯呵吱吱吱發表於2019-02-16

在原文的基礎上加了一點參考資料

問題的引出

event loop都不陌生,是指主執行緒從“任務佇列”中迴圈讀取任務,比如

例1:

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

console.log(2)

//輸出2,1

在上述的例子中,我們明白首先執行主執行緒中的同步任務,當主執行緒任務執行完畢後,再從event loop中讀取任務,因此先輸出2,再輸出1。

event loop讀取任務的先後順序,取決於任務佇列(Job queue)中對於不同任務讀取規則的限定。比如下面一個例子:

例2:

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

Promise.resolve().then(function () {
  console.log(2);
});
console.log(1);
//輸出為  1  2 3

先輸出1,沒有問題,因為是同步任務在主執行緒中優先執行,這裡的問題是setTimeout和Promise.then任務的執行優先順序是如何定義的。

2 . Job queue中的執行順序
在Job queue中的佇列分為兩種型別:macro-task和microTask。我們舉例來看執行順序的規定,我們設

macro-task佇列包含任務: a1, a2 , a3
micro-task佇列包含任務: b1, b2 , b3

執行順序為,首先執行marco-task佇列開頭的任務,也就是 a1 任務,執行完畢後,在執行micro-task佇列裡的所有任務,也就是依次執行b1, b2 , b3,執行完後清空micro-task中的任務,接著執行marco-task中的第二個任務,依次迴圈。

瞭解完了macro-task和micro-task兩種佇列的執行順序之後,我們接著來看,真實場景下這兩種型別的佇列裡真正包含的任務(我們以node V8引擎為例),在node V8中,這兩種型別的真實任務順序如下所示:

macro-task佇列真實包含任務:

script(主程式程式碼),setTimeout, setInterval, setImmediate, I/O, UI rendering

micro-task佇列真實包含任務:
process.nextTick, Promises, Object.observe, MutationObserver

由此我們得到的執行順序應該為:

script(主程式程式碼)—>process.nextTick—>Promises…——>setTimeout——>setInterval——>setImmediate——> I/O——>UI rendering

在ES6中macro-task佇列又稱為ScriptJobs,而micro-task又稱PromiseJobs

3 . 真實環境中執行順序的舉例
(1) setTimeout和promise
例3:

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

Promise.resolve().then(function () {
console.log(2);
});

console.log(1);

我們先以第1小節的例子為例,這裡遵循的順序為:

script(主程式程式碼)——>promise——>setTimeout
對應的輸出依次為:1 ——>2————>3

(2) process.nextTick和promise、setTimeout
例子4:

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

new Promise(function(resolve,reject){
   console.log(2);
   resolve();
}).then(function(){console.log(3)
}).then(function(){console.log(4)});

process.nextTick(function(){console.log(5)});

console.log(6);
//輸出2,6,5,3,4,1

這個例子就比較複雜了,這裡要注意的一點在定義promise的時候,promise構造部分是同步執行的,這樣問題就迎刃而解了。

首先分析Job queue的執行順序:

script(主程式程式碼)——>process.nextTick——>promise——>setTimeout

I) 主體部分: 定義promise的構造部分是同步的,
因此先輸出2 ,主體部分再輸出6(同步情況下,就是嚴格按照定義的先後順序)

II)process.nextTick: 輸出5

III)promise: 這裡的promise部分,嚴格的說其實是promise.then部分,輸出的是3,4

IV) setTimeout : 最後輸出1

綜合的執行順序就是: 2——>6——>5——>3——>4——>1

(3)更復雜的例子

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

new Promise(function(resolve,reject){
   console.log(2);
   setTimeout(function(){resolve()},0)
}).then(function(){console.log(3)
}).then(function(){console.log(4)});

process.nextTick(function(){console.log(5)});

console.log(6);

//輸出的是  2 6 5 1 3 4

這種情況跟我們(2)中的例子,區別在於promise的構造中,沒有同步的resolve,因此promise.then在當前的執行佇列中是不存在的,只有promise從pending轉移到resolve,才會有then方法,而這個resolve是在一個setTimout時間中完成的,因此3,4最後輸出。

知識點參考

  1. process.nextTick(callback)

    process.nextTick()方法將 callback 新增到”next tick 佇列”。 一旦當前事件輪詢佇列的任務全部完成,在next tick佇列中的所有callbacks會被依次呼叫。

    這種方式不是setTimeout(fn, 0)的別名。它更加有效率。事件輪詢隨後的ticks 呼叫,會在任何I/O事件(包括定時器)之前執行。

    每次事件輪詢後,在額外的I/O執行前,next tick佇列都會優先執行。 遞迴呼叫nextTick callbacks 會阻塞任何I/O操作,就像一個while(true) 迴圈一樣。

  function definitelyAsync(arg, cb) {
  if (arg) {
    process.nextTick(cb);
    return;
  }

  fs.stat(`file`, cb);  //涉及io操作
}
//這裡process.nextTick就會阻塞io操作

相關文章