本文受:
(上)https://zhuanlan.zhihu.com/p/26229293
(下)https://zhuanlan.zhihu.com/p/26238030
這兩篇知乎博文的啓發,對事件循環機制進行分析與總結。
對於JavaScript中的單線程,擁有唯一的一個事件循環,事件循環就像是一個
while (true) {
// 執行一些代碼...
}
一樣,不斷地去執行函數調用棧中的代碼。在JavaScript代碼執行時,除了依靠函數調用棧來搞定函數的執行順序外,還要依靠任務隊列來搞定另外一些代碼的執行,但最終,任務隊列中的代碼總是會放到調用棧中去執行。
下面說一下事件循環機制中的幾個重要的內容:
- 一個線程中,事件循環是唯一的,但是任務隊列可以擁有多個。
- 任務隊列又分爲macro-task(宏任務)與micro-task(微任務),在最新標準中,它們被分別稱爲task與jobs。
- macro-task大概包括:script(整體代碼), setTimeout, setInterval, setImmediate, I/O, UI rendering。
- micro-task大概包括: process.nextTick, Promise, Object.observe(已廢棄), MutationObserver(html5新特性)。
- setTimeout/Promise等我們稱之爲任務源。而進入任務隊列的是他們指定的具體執行任務。
- 來自不同任務源的任務會進入到不同的任務隊列。其中setTimeout與setInterval是同源的。
- 事件循環的順序,決定了JavaScript代碼的執行順序。它從script(整體代碼)開始第一次循環。之後全局上下文進入函數調用棧。直到調用棧清空(只剩全局),然後執行所有的micro-task。當所有可執行的micro-task執行完畢之後。循環再次從macro-task開始,找到其中一個任務隊列執行完畢,然後再執行所有的micro-task,這樣一直循環下去。
- 其中每一個任務的執行,無論是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
可以自己對照着一步一步去分析,非常有助於理解事件循環中的機制。