我在學習瀏覽器和NodeJS的Event Loop時看了大量的文章,那些文章都寫的很好,可是每每是每篇文章有那麼幾個關鍵的點,不少篇文章湊在一塊兒綜合來看,才能夠對這些概念有較爲深刻的理解。html
因而,我在看了大量文章以後,想要寫這麼一篇博客,不採用官方的描述,結合本身的理解以及示例代碼,用最通俗的語言表達出來。但願你們能夠經過這篇文章,瞭解到Event Loop究竟是一種什麼機制,瀏覽器和NodeJS的Event Loop又有什麼區別。若是在文中出現書寫錯誤的地方,歡迎你們留言一塊兒探討。html5
(PS:說到Event Loop確定會提到Promise,我根據Promise A+規範本身實現了一個簡易Promise庫,源碼放到Github上,你們有須要的能夠當作參考,後續我也會也寫一篇博客來說Promise,若是對你有用,就請給個Star吧~)node
event loop是一個執行模型,在不一樣的地方有不一樣的實現。瀏覽器和NodeJS基於不一樣的技術實現了各自的Event Loop。git
宏隊列,macrotask,也叫tasks。 一些異步任務的回調會依次進入macro task queue,等待後續被調用,這些異步任務包括:github
微隊列,microtask,也叫jobs。 另外一些異步任務的回調會依次進入micro task queue,等待後續被調用,這些異步任務包括:web
(注:這裏只針對瀏覽器和NodeJS)segmentfault
咱們先來看一張圖,再看完這篇文章後,請返回來再仔細看一下這張圖,相信你會有更深的理解。api
這張圖將瀏覽器的Event Loop完整的描述了出來,我來說執行一個JavaScript代碼的具體流程:promise
能夠看到,這就是瀏覽器的事件循環Event Loop瀏覽器
這裏概括3個重點:
好了,概念性的東西就這麼多,來看幾個示例代碼,測試一下你是否掌握了:
console.log(1);
setTimeout(() => {
console.log(2);
Promise.resolve().then(() => {
console.log(3)
});
});
new Promise((resolve, reject) => {
console.log(4)
resolve(5)
}).then((data) => {
console.log(data);
})
setTimeout(() => {
console.log(6);
})
console.log(7);
複製代碼
這裏結果會是什麼呢?運用上面瞭解到的知識,先本身作一下試試看。
// 正確答案
1
4
7
5
2
3
6
複製代碼
你答對了嗎?
咱們來分析一下整個流程:
Step 1
console.log(1)
複製代碼
Stack Queue: [console]
Macrotask Queue: []
Microtask Queue: []
打印結果:
1
Step 2
setTimeout(() => {
// 這個回調函數叫作callback1,setTimeout屬於macrotask,因此放到macrotask queue中
console.log(2);
Promise.resolve().then(() => {
console.log(3)
});
});
複製代碼
Stack Queue: [setTimeout]
Macrotask Queue: [callback1]
Microtask Queue: []
打印結果:
1
Step 3
new Promise((resolve, reject) => {
// 注意,這裏是同步執行的,若是不太清楚,能夠去看一下我開頭本身實現的promise啦~~
console.log(4)
resolve(5)
}).then((data) => {
// 這個回調函數叫作callback2,promise屬於microtask,因此放到microtask queue中
console.log(data);
})
複製代碼
Stack Queue: [promise]
Macrotask Queue: [callback1]
Microtask Queue: [callback2]
打印結果:
1
4
Step 5
setTimeout(() => {
// 這個回調函數叫作callback3,setTimeout屬於macrotask,因此放到macrotask queue中
console.log(6);
})
複製代碼
Stack Queue: [setTimeout]
Macrotask Queue: [callback1, callback3]
Microtask Queue: [callback2]
打印結果:
1
4
Step 6
console.log(7)
複製代碼
Stack Queue: [console]
Macrotask Queue: [callback1, callback3]
Microtask Queue: [callback2]
打印結果:
1
4
7
Step 7
console.log(data) // 這裏data是Promise的決議值5
複製代碼
Stack Queue: [callback2]
Macrotask Queue: [callback1, callback3]
Microtask Queue: []
打印結果:
1
4
7
5
Step 8
console.log(2)
複製代碼
Stack Queue: [callback1]
Macrotask Queue: [callback3]
Microtask Queue: []
打印結果:
1
4
7
5
2
可是,執行callback1的時候又遇到了另外一個Promise,Promise異步執行完後在microtask queue中又註冊了一個callback4回調函數
Step 9
Promise.resolve().then(() => {
// 這個回調函數叫作callback4,promise屬於microtask,因此放到microtask queue中
console.log(3)
});
複製代碼
Stack Queue: [promise]
Macrotask v: [callback3]
Microtask Queue: [callback4]
打印結果:
1
4
7
5
2
Step 10
console.log(3)
複製代碼
Stack Queue: [callback4]
Macrotask Queue: [callback3]
Microtask Queue: []
打印結果:
1
4
7
5
2
3
Step 11
console.log(6)
複製代碼
Stack Queue: [callback3]
Macrotask Queue: []
Microtask Queue: []
打印結果:
1
4
7
5
2
3
6
Stack Queue: []
Macrotask Queue: []
Microtask Queue: []
最終打印結果:
1
4
7
5
2
3
6
由於是第一個例子,因此這裏分析的比較詳細,你們仔細看一下,接下來咱們再來一個例子:
console.log(1);
setTimeout(() => {
console.log(2);
Promise.resolve().then(() => {
console.log(3)
});
});
new Promise((resolve, reject) => {
console.log(4)
resolve(5)
}).then((data) => {
console.log(data);
Promise.resolve().then(() => {
console.log(6)
}).then(() => {
console.log(7)
setTimeout(() => {
console.log(8)
}, 0);
});
})
setTimeout(() => {
console.log(9);
})
console.log(10);
複製代碼
最終輸出結果是什麼呢?參考前面的例子,好好想想......
// 正確答案
1
4
10
5
6
7
2
3
9
8
複製代碼
相信你們都答對了,這裏的關鍵在前面已經提過:
在執行微隊列microtask queue中任務的時候,若是又產生了microtask,那麼會繼續添加到隊列的末尾,也會在這個週期執行,直到microtask queue爲空中止。
注:固然若是你在microtask中不斷的產生microtask,那麼其餘宏任務macrotask就沒法執行了,可是這個操做也不是無限的,拿NodeJS中的微任務process.nextTick()來講,它的上限是1000個,後面咱們會講到。
瀏覽器的Event Loop就說到這裏,下面咱們看一下NodeJS中的Event Loop,它更復雜一些,機制也不太同樣。
先來看一張libuv的結構圖:
NodeJS的Event Loop中,執行宏隊列的回調任務有6個階段,以下圖:
各個階段執行的任務以下:
NodeJS中宏隊列主要有4個
由上面的介紹能夠看到,回調事件主要位於4個macrotask queue中:
這4個都屬於宏隊列,可是在瀏覽器中,能夠認爲只有一個宏隊列,全部的macrotask都會被加到這一個宏隊列中,可是在NodeJS中,不一樣的macrotask會被放置在不一樣的宏隊列中。
NodeJS中微隊列主要有2個:
在瀏覽器中,也能夠認爲只有一個微隊列,全部的microtask都會被加到這一個微隊列中,可是在NodeJS中,不一樣的microtask會被放置在不一樣的微隊列中。
具體能夠經過下圖加深一下理解:
大致解釋一下NodeJS的Event Loop過程:
關於NodeJS的macrotask queue和microtask queue,我畫了兩張圖,你們做爲參考:
好啦,概念理解了咱們經過幾個例子來實戰一下:
第一個例子
console.log('start');
setTimeout(() => { // callback1
console.log(111);
setTimeout(() => { // callback2
console.log(222);
}, 0);
setImmediate(() => { // callback3
console.log(333);
})
process.nextTick(() => { // callback4
console.log(444);
})
}, 0);
setImmediate(() => { // callback5
console.log(555);
process.nextTick(() => { // callback6
console.log(666);
})
})
setTimeout(() => { // callback7
console.log(777);
process.nextTick(() => { // callback8
console.log(888);
})
}, 0);
process.nextTick(() => { // callback9
console.log(999);
})
console.log('end');
複製代碼
更新 2018.9.20
上面這段代碼你執行的結果可能會有多種狀況,緣由解釋以下。
setTimeout(fn, 0)不是嚴格的0,通常是setTimeout(fn, 3)或什麼,會有必定的延遲時間,當setTimeout(fn, 0)和setImmediate(fn)出如今同一段同步代碼中時,就會存在兩種狀況。
第1種狀況:同步代碼執行完了,Timer還沒到期,setImmediate回調先註冊到Check Queue中,開始執行微隊列,而後是宏隊列,先從Timers Queue中開始,發現沒回調,往下走直到Check Queue中有回調,執行,而後timer到期(只要在執行完Timer Queue後到期效果就都同樣),timer回調註冊到Timers Queue中,下一輪循環執行到Timers Queue中才能執行那個timer 回調;因此,這種狀況下,setImmediate(fn)回調先於setTimeout(fn, 0)回調執行。
第2種狀況:同步代碼還沒執行完,timer先到期,timer回調先註冊到Timers Queue中,執行到setImmediate了,它的回調再註冊到Check Queue中。 而後,同步代碼執行完了,執行微隊列,而後開始先執行Timers Queue,先執行Timer 回調,再到Check Queue,執行setImmediate回調;因此,這種狀況下,setTimeout(fn, 0)回調先於setImmediate(fn)回調執行。
因此,在同步代碼中同時調setTimeout(fn, 0)和setImmediate狀況是不肯定的,可是若是把他們放在一個IO的回調,好比readFile('xx', function () {// ....})回調中,那麼IO回調是在IO Queue中,setTimeout到期回調註冊到Timers Queue,setImmediate回調註冊到Check Queue,IO Queue執行完到Check Queue,timer Queue獲得下個週期,因此setImmediate回調這種狀況下確定比setTimeout(fn, 0)回調先執行。
綜上,這個例子是不太好的,setTimeout(fn, 0)和setImmediate(fn)若是想要保證結果惟一,就放在一個IO Callback中吧,上面那段代碼能夠把全部它倆同步執行的代碼都放在一個IO Callback中,結果就惟一了。
更新結束
請運用前面學到的知識,仔細分析一下......
// 正確答案
start
end
999
111
777
444
888
555
333
666
222
複製代碼
你答對了嗎?咱們來一塊兒分析一下:
宏隊列
Timers Queue: [callback1, callback7]
Check Queue: [callback5]
IO Callback Queue: []
Close Callback Queue: []
微隊列
Next Tick Queue: [callback9]
Other Microtask Queue: []
打印結果
start
end
宏隊列
Timers Queue: [callback1, callback7]
Check Queue: [callback5]
IO Callback Queue: []
Close Callback Queue: []
微隊列
Next Tick Queue: []
Other Microtask Queue: []
打印結果
start
end
999
宏隊列
Timers Queue: [callback2]
Check Queue: [callback5, callback3]
IO Callback Queue: []
Close Callback Queue: []
微隊列
Next Tick Queue: [callback4, callback8]
Other Microtask Queue: []
打印結果
start
end
999
111
777
宏隊列
Timers Queue: [callback2]
Check Queue: [callback5, callback3]
IO Callback Queue: []
Close Callback Queue: []
微隊列
Next Tick Queue: []
Other Microtask Queue: []
打印結果
start
end
999
111
777
444
888
宏隊列
Timers Queue: [callback2]
Check Queue: []
IO Callback Queue: []
Close Callback Queue: []
微隊列
Next Tick Queue: [callback6]
Other Microtask Queue: []
打印結果
start
end
999
111
777
444
888
555
333
宏隊列
Timers Queue: [callback2]
Check Queue: []
IO Callback Queue: []
Close Callback Queue: []
微隊列
Next Tick Queue: [callback6]
Other Microtask Queue: []
打印結果
start
end
999
111
777
444
888
555
333
宏隊列
Timers Queue: []
Check Queue: []
IO Callback Queue: []
Close Callback Queue: []
微隊列
Next Tick Queue: [callback6]
Other Microtask Queue: []
最終結果
start
end
999
111
777
444
888
555
333
666
222
以上就是這道題目的詳細分析,若是沒有明白,必定要多看幾回。
下面引入Promise再來看一個例子:
console.log('1');
setTimeout(function() {
console.log('2');
process.nextTick(function() {
console.log('3');
})
new Promise(function(resolve) {
console.log('4');
resolve();
}).then(function() {
console.log('5')
})
})
new Promise(function(resolve) {
console.log('7');
resolve();
}).then(function() {
console.log('8')
})
process.nextTick(function() {
console.log('6');
})
setTimeout(function() {
console.log('9');
process.nextTick(function() {
console.log('10');
})
new Promise(function(resolve) {
console.log('11');
resolve();
}).then(function() {
console.log('12')
})
})
複製代碼
你們仔細分析,相比於上一個例子,這裏因爲存在Promise,因此Other Microtask Queue中也會有回調任務的存在,執行到微任務階段時,先執行Next Tick Queue中的全部任務,再執行Other Microtask Queue中的全部任務,而後纔會進入下一個階段的宏任務。明白了這一點,相信你們均可以分析出來,下面直接給出正確答案,若有疑問,歡迎留言和我討論。
// 正確答案
1
7
6
8
2
4
9
11
3
10
5
12
複製代碼
二者的執行順序要根據當前的執行環境才能肯定:
Promises, process.nextTick And setImmediate