
深入淺出JavaScript事件循環機制(下)
柳兮 7 個月前
在上一篇文章裡面我大緻介紹了JavaScript的事件循環機制,但是最後還留下了一段代碼和幾個問題。
那我們先從這段代碼開始看哇
(function test() {
setTimeout(function() {console.log(4)}, 0);
new Promise(function executor(resolve) {
console.log(1);
for( var i=0 ; i<10000 ; i++ ) {
i == 9999 && resolve();
}
console.log(2);
}).then(function() {
console.log(5);
});
console.log(3);
})()
在這段代碼裡面,setTimeout和Promise都被稱之為任務源,來自不同任務源的回調函數會被放進不同的任務隊列裡面。
setTimeout的回調函數被放進setTimeout的任務隊列之中。而對于Promise,它的回調函數并不是傳進去的executer函數,而是其異步執行的then方法裡面的參數,被放進Promise的任務隊列之中。也就是說Promise的第一個參數并不會被放進Promise的任務隊列之中,而會在目前隊列就執行。
其中setTimeout和Promise的任務隊列叫做macro-task(宏任務),當然如我們所想,還有micro-task(微任務)。
- macro-task包括:script(整體代碼), setTimeout, setInterval, setImmediate, I/O, UI rendering。
- micro-task包括:process.nextTick, Promises, Object.observe, MutationObserver
其中上面的setImmediate和process.nextTick是Node.JS裡面的API,浏覽器裡面并沒有,這裡就當舉例,不必糾結具體是怎麼實作的。
事件循環的順序是從script開始第一次循環,随後全局上下文進入函數調用棧,碰到macro-task就将其交給處理它的子產品處理完之後将回調函數放進macro-task的隊列之中,碰到micro-task也是将其回調函數放進micro-task的隊列之中。直到函數調用棧清空隻剩全局執行上下文,然後開始執行所有的micro-task。當所有可執行的micro-task執行完畢之後。循環再次執行macro-task中的一個任務隊列,執行完之後再執行所有的micro-task,就這樣一直循環。
分析執行過程
下面分析的思路按照波同學之前所寫的深入核心,詳解事件循環機制中的思路進行分析。以之前的栗子作為分析的對象,來分析事件循環機制究竟是怎麼執行代碼的
(function test() {
setTimeout(function() {console.log(4)}, 0);
new Promise(function executor(resolve) {
console.log(1);
for( var i=0 ; i<10000 ; i++ ) {
i == 9999 && resolve();
}
console.log(2);
}).then(function() {
console.log(5);
});
console.log(3);
})()
注意下面所有圖中的setTimeout任務隊和最後的函數調用棧中存放的都是setTimeout的回調函數,并不是整個setTimeout定時器。 1.首先,script任務源先執行,全局上下文入棧。
2.script任務源的代碼在執行時遇到setTimeout,作為一個macro-task,将其回調函數放入自己的隊列之中。
2.script任務源的代碼在執行時遇到setTimeout,作為一個macro-task,将其回調函數放入自己的隊列之中。
3.script任務源的代碼在執行時遇到Promise執行個體。Promise構造函數中的第一個參數是在目前任務直接執行不會被放入隊列之中,是以此時輸出 1 。
3.script任務源的代碼在執行時遇到Promise執行個體。Promise構造函數中的第一個參數是在目前任務直接執行不會被放入隊列之中,是以此時輸出 1 。
4.在for循環裡面遇到resolve函數,函數入棧執行之後出棧,此時Promise的狀态變成Fulfilled。代碼接着執行遇到console.log(2),輸出2。
4.在for循環裡面遇到resolve函數,函數入棧執行之後出棧,此時Promise的狀态變成Fulfilled。代碼接着執行遇到console.log(2),輸出2。
5.接着執行,代碼遇到then方法,其回調函數作為micro-task入棧,進入Promise的任務隊列之中。
;6.代碼接着執行,此時遇到console.log(3),輸出3。
6.代碼接着執行,此時遇到console.log(3),輸出3。
7.輸出3之後第一個宏任務script的代碼執行完畢,這時候開始開始執行所有在隊列之中的micro-task。then的回調函數入棧執行完畢之後出棧,這時候輸出5
7.輸出3之後第一個宏任務script的代碼執行完畢,這時候開始開始執行所有在隊列之中的micro-task。then的回調函數入棧執行完畢之後出棧,這時候輸出5
8.這時候所有的micro-task執行完畢,第一輪循環結束。第二輪循環從setTimeout的任務隊列開始,setTimeout的回調函數入棧執行完畢之後出棧,此時輸出4。
8.這時候所有的micro-task執行完畢,第一輪循環結束。第二輪循環從setTimeout的任務隊列開始,setTimeout的回調函數入棧執行完畢之後出棧,此時輸出4。
總結
總的來說就是:
- 不同的任務會放進不同的任務隊列之中。
- 先執行macro-task,等到函數調用棧清空之後再執行所有在隊列之中的micro-task。
- 等到所有micro-task執行完之後再從macro-task中的一個任務隊列開始執行,就這樣一直循環。
- 當有多個macro-task(micro-task)隊列時,事件循環的順序是按上文macro-task(micro-task)的分類中書寫的順序執行的。
測試
說到這裡,我們應該都明白了,下面是一個複雜的代碼段(改自深入核心,詳解事件循環機制),裡面有混雜着的micro-task和macro-task,自己畫圖試試流程哇,然後再用node執行看看輸出的順序是否一緻。
console.log('golb1');
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')
})
})
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')
})
setTimeout(function() {
console.log('timeout1_timeout1');
process.nextTick(function() {
console.log('timeout1_timeout1_nextTick');
})
setImmediate(function() {
console.log('timeout1_setImmediate1');
})
});
})
new Promise(function(resolve) {
console.log('glob1_promise');
resolve();
}).then(function() {
console.log('glob1_then')
})
process.nextTick(function() {
console.log('glob1_nextTick');
})