深入理解Node.js中的事件循環(帶例子和分析)

本文受:

(上)https://zhuanlan.zhihu.com/p/26229293

(下)https://zhuanlan.zhihu.com/p/26238030

這兩篇知乎博文的啓發,對事件循環機制進行分析與總結。

 

對於JavaScript中的單線程,擁有唯一的一個事件循環,事件循環就像是一個

while (true) {
    // 執行一些代碼...
}

一樣,不斷地去執行函數調用棧中的代碼。在JavaScript代碼執行時,除了依靠函數調用棧來搞定函數的執行順序外,還要依靠任務隊列來搞定另外一些代碼的執行,但最終,任務隊列中的代碼總是會放到調用棧中去執行。

 

下面說一下事件循環機制中的幾個重要的內容:

  1. 一個線程中,事件循環是唯一的,但是任務隊列可以擁有多個。
  2. 任務隊列又分爲macro-task(宏任務)與micro-task(微任務),在最新標準中,它們被分別稱爲task與jobs。
  3. macro-task大概包括:script(整體代碼), setTimeout, setInterval, setImmediate, I/O, UI rendering。
  4. micro-task大概包括: process.nextTick, Promise, Object.observe(已廢棄), MutationObserver(html5新特性)。
  5. setTimeout/Promise等我們稱之爲任務源。而進入任務隊列的是他們指定的具體執行任務。
  6. 來自不同任務源的任務會進入到不同的任務隊列。其中setTimeout與setInterval是同源的。
  7. 事件循環的順序,決定了JavaScript代碼的執行順序。它從script(整體代碼)開始第一次循環。之後全局上下文進入函數調用棧。直到調用棧清空(只剩全局),然後執行所有的micro-task。當所有可執行的micro-task執行完畢之後。循環再次從macro-task開始,找到其中一個任務隊列執行完畢,然後再執行所有的micro-task,這樣一直循環下去。
  8. 其中每一個任務的執行,無論是macro-task還是micro-task,都是藉助函數調用棧來完成。

首先,我們通過一個稍微簡單一點的例子去分析,後面熟悉了之後再上一個稍微複雜一點的例子。

// 爲了方便理解,我以打印出來的字符作爲當前的任務名稱
setTimeout(function() {
    console.log('timeout1');
})

new Promise(function(resolve) {
    console.log('promise1');
    for(var i = 0; i < 1000; i++) {
        i == 99 && resolve();
    }
    console.log('promise2');
}).then(function() {
    console.log('then1');
})

console.log('global1');

分析過程:

首先,事件循環從宏任務開始,這個宏任務隊列中,只有一個script(整體代碼)任務。每一個任務的執行順序,都依靠函數調用棧考完成,當遇到任務源時,則先分發到對應的隊列中。

第二步:script任務的執行首先遇到了setTimeout,setTimeout作爲一個宏任務,它將任務分發到它對應的宏任務隊列中(假設爲setTimeout隊列)

第三步:script執行到Promise處,Promise構造函數中的第一個參數是在new的時候執行的,即在當前任務直接執行而不會被放入任務的隊列當中,裏面的參數進入函數調用棧中執行,for循環不會進入任何隊列,因此代碼會依次執行。後續的then()中的函數則會被分發到微任務中的Promise隊列中去。此時打印了:promise1, promise2。

第四步:script繼續往下執行, 到最後一句的時候輸出global1,全局任務執行完畢。第一個宏任務script執行完畢之後,就開始執行所有的可執行的微任務。這時候微任務中只有Promise隊列中的一個任務then,因此直接執行,結果輸出then1,當然它的執行也是進入函數調用棧中執行的。

第五步:當所有的微任務都執行完畢之後,表示第一輪循環就結束了,這個時候就得開始第二輪的循環。第二輪循環仍然從宏任務開始。這時候發現宏任務中只有setTimeout隊列中還有一個timeout1的任務等待執行,因此直接執行。

這時候宏任務隊列和微任務隊列中都沒有任務了,所以代碼就不會再輸出其他東西了。

所以輸出順序爲:promise1, promise2, global1, then1, timeout1

 

上面這個簡單例子分析完成後,我們開始分析稍微複雜點的例子:

console.log('golbal1');

setTimeout(function() {
    console.log('timeout1');
    process.nextTick(function() {
        console.log('timeout1_nextTick');
    })
    new Promise(function(resolve) {
        console.log('timeout1_promise');
        resolve();
    }).then(function() {
        console.log('timeout1_then')
    })
})

setImmediate(function() {
    console.log('immediate1');
    process.nextTick(function() {
        console.log('immediate1_nextTick');
    })
    new Promise(function(resolve) {
        console.log('immediate1_promise');
        resolve();
    }).then(function() {
        console.log('immediate1_then')
    })
})

process.nextTick(function() {
    console.log('glob1_nextTick');
})
new Promise(function(resolve) {
    console.log('glob1_promise');
    resolve();
}).then(function() {
    console.log('glob1_then')
})

setTimeout(function() {
    console.log('timeout2');
    process.nextTick(function() {
        console.log('timeout2_nextTick');
    })
    new Promise(function(resolve) {
        console.log('timeout2_promise');
        resolve();
    }).then(function() {
        console.log('timeout2_then')
    })
})

process.nextTick(function() {
    console.log('glob2_nextTick');
})
new Promise(function(resolve) {
    console.log('glob2_promise');
    resolve();
}).then(function() {
    console.log('glob2_then')
})

setImmediate(function() {
    console.log('immediate2');
    process.nextTick(function() {
        console.log('immediate2_nextTick');
    })
    new Promise(function(resolve) {
        console.log('immediate2_promise');
        resolve();
    }).then(function() {
        console.log('immediate2_then')
    })
})

分析過程:

第一步:首先宏任務script執行,全局入棧,輸出global1

第二步:往下執行,遇到setTimeout,timeout1進入宏任務setTimeout隊列中。

第三步:往下執行,遇到setImmediate,immediate1進入宏任務setImmediate隊列中。

第四步:往下執行,遇到process.nextTick,是微任務,global1_nextTick進入微任務process_nextTick隊列中。

第五步:往下執行,遇到Promise,Promise構造函數中的參數直接執行,因此輸出global1_promise,global1_then進入微任務的Promise隊列中。

第六步:往下執行,遇到setTimeout,timeout2進入宏任務setTimeout隊列中。

第七步:往下執行,遇到process.nextTick,global2_nextTick進入微任務process_nextTick隊列中。

第八步:往下執行,遇到Promise,構造函數中的參數直接執行,因此輸出global2_promise,global2_then進入微任務的Promise隊列中。

第九步:往下執行,遇到setImmediate,immediate2進入宏任務setImmediate隊列中。

此時整個script中的代碼就執行完成了,如下圖所示。執行過程中,遇到不同的任務分發器,就將任務分發到各自對應的隊列中去。就下來,將會執行所有的微任務隊列中的任務。

其中,nextTick隊列會比Promise的先執行,nextTick中的可執行任務執行完成之後,纔會開始執行Promise隊列中的任務。

 

第十步:global1_nextTick進入函數調用棧執行,輸出global1_nextTick1,緊接着global2_nextTick進入調用棧執行,輸出global2_nextTick

第十一步:輪到Promise隊列中的執行,global1_then進入執行棧執行,輸出global1_then,然後接着global2_then進入執行棧執行,輸出global2_then

第十二步:此時微任務隊列爲空,所有可執行的微任務已執行完畢,表示這一輪循環已經結束了,下一輪循環繼續從宏任務開始執行。這時候,從SetTimeout隊列中開始彈出任務timeout1到執行棧中執行,輸出timeout1。往下執行,遇到timeout1_nextTick,放入nextTick隊列中。繼續往下執行,遇到Promise,timeout1_promise進入執行棧執行,輸出timeout1_promise,把timeout1_then放入Promise隊列中。

第十三步:執行完上一步這一個宏任務之後,就又開始執行微任務隊列中的所有微任務,此時微任務隊列中nextTick隊列和Promise隊列中均有任務,先後輸出timeput1_nextTick輸出timeout1_then

第十四步:此時所有微任務隊列爲空,開始執行下一輪循環,timeout2進入執行棧執行,輸出timeout2。往下執行遇到nextTick,timeout2_nextTick進入nextTick隊列中,繼續執行,遇到Promise,輸出timeout2_promise,timeout2_then進入Promise隊列中。此時這一個宏任務又執行完成。

第十五步:執行完上一步這一個宏任務之後,就又開始執行微任務隊列中的所有微任務,此時微任務隊列中nextTick隊列和Promise隊列中均有任務,先後輸出timeput2_nextTick輸出timeout2_then

第十六步:此時setTimeout隊列爲空,開始執行setImmediate隊列中的宏任務。immediate1進入執行棧,輸出immediate1,往下執行遇到nextTick,immediate1_nextTick放入nextTick隊列中,繼續往下執行,遇到Promise,輸出immediate1_promise,將immediate1_then放入Promise隊列。

第十七步:此時一個宏任務執行完成,開始執行所有的微任務,輸出immediate1_nextTick輸出immediate1_then

第十八步:微任務隊列爲空,又開始下一個循環,開始執行最後一個宏任務immediate2,輸出immediate2,往下執行,遇到nextTick,將immediate2_nextTick放入nextTick隊列中,繼續往下執行,遇到Promise,輸出immediate2_promise,將immediate2_then放入Promise隊列中。此時最後的宏任務已執行完畢。

第十九步:宏任務執行完畢,開始執行所有的微任務。先後輸出immediate_nextTick輸出immediate2_then

第二十步:開始下一輪循環,此時宏任務隊列爲空,執行完成。

因此,最後的輸出順序是:

golb1
glob1_promise
glob2_promise
glob1_nextTick
glob2_nextTick
glob1_then
glob2_then
timeout1
timeout1_promise
timeout1_nextTick
timeout1_then
timeout2
timeout2_promise
timeout2_nextTick
timeout2_then
immediate1
immediate1_promise
immediate1_nextTick
immediate1_then
immediate2
immediate2_promise
immediate2_nextTick
immediate2_then

可以自己對照着一步一步去分析,非常有助於理解事件循環中的機制。

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章