1. 什么是eventLoop?
它是一個在 JavaScript 引擎等待任務,執行任務和進入休眠狀態等待更多任務這幾個狀態之間轉換的無限循環。 我們都知道JavaScript引擎是單線程的,至於為什么是單線程主要是出於JavaScript的使用場景考慮,作為瀏覽器的腳本語言,js的主要任務是主要是實現用戶與瀏覽器的交互,以及操作dom,如果設計成多線程會增加復雜的同步問題。想象一個場景:多個線程同時操作dom,瀏覽器渲染引擎該使用哪個線程的結果。當然為了利用多核CPU的計算能力,HTML5提出Web Worker標准,允許JavaScript腳本創建多個線程,但是子線程完全受主線程控制,且不得操作DOM。所以,這個新標准並沒有改變JavaScript單線程的本質。
雖然JS是單線程的,但瀏覽器卻是多線程,其中幾個典型的線程已經在圖中表示出來,而eventLoop就是溝通JS引擎線程和瀏覽器線程的橋梁,也是瀏覽器實現異步非阻塞模型的關鍵。
2. 宏隊列和微隊列:
宏隊列,macrotask,也叫tasks。 一些異步任務的回調會依次進入macro task queue,等待后續被調用,這些異步任務包括:
- setTimeout
- setInterval
- setImmediate (Node獨有)
- requestAnimationFrame (瀏覽器獨有)
- I/O
- UI rendering (瀏覽器獨有)
微隊列,microtask,也叫jobs。 另一些異步任務的回調會依次進入micro task queue,等待后續被調用,這些異步任務包括:
- process.nextTick (Node獨有)
- Promise
- Object.observe
- MutationObserver
3. 瀏覽器事件循環流程簡圖
3.1 瀏覽器EventLoop的具體流程:
- js引擎將所有代碼放入執行棧,並依次彈出並執行,這些任務有的是同步有的是異步(宏任務或微任務)。
- 如果在執行 棧中代碼時發現宏任務則交個瀏覽器相應的線程去處理,瀏覽器線程在正確的時機(比如定時器最短延遲時間)將宏任務的消息(或稱之為回調函數)推入宏任務隊列。而宏任務隊列中的任務只有執行棧為空時才會執行。
- 如果執行 棧中的代碼時發現微任務則推入微任務隊列,和宏任務隊列一樣,微任務隊列的任務也在執行棧為空時才會執行,但是微任務始終比宏任務先執行。
- 當執行棧為空時,eventLoop轉到微任務隊列處,依次彈出首個任務放入執行棧並執行,如果在執行的過程中又有微任務產生則推入隊列末尾,這樣循環直到微任務隊列為空。
- 當執行棧和微任務隊列都為空時,eventLoop轉到宏任務隊列,並取出隊首的任務放入執行棧執行。需要注意的是宏任務每次循環只執行一個。
- 重復1-5過程
- ...直到棧和隊列都為空時,代碼執行結束。引擎休眠等待直至下次任務出現。
3.2 在這個過程中有三個重點:
1. 宏任務每次只取一個,執行之后馬上執行微任務。
2. 微任務會依次執行,直到微任務隊列為空。
3. 圖中沒有畫UI rendering的節點,因為這個是由瀏覽器自行判斷決定的,但是只要執行UI rendering,它的節點是在執行完所有的microtask之后,下一個macrotask之前,緊跟着執行UI render。
看到這里相信任何事件循環判斷的題都難不倒你了,做個小測試檢驗一下吧:
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) }, 0);
console.log(10)
答案:1,4,10,5,6,7,2,3,9,8 你做對了嗎? 如果還不明白的話對照圖在走一次。
4. Node.JS事件循環流程簡圖
可以看出Node.JS的事件循環比瀏覽器端復雜很多。
5. NodeJS中的宏隊列和微隊列
5.1 事實上NodeJS中執行宏隊列的回調任務有6個階段,按如下方式依次執行:
- timers階段:這個階段執行setTimeout和setInterval預定的callback。
- I/O callback階段:執行除了close事件的callbacks、被timers設定的callbacks、setImmediate()設定的callbacks這些之外的callbacks。
- idle, prepare階段:僅node內部使用。
- poll階段:獲取新的I/O事件,適當的條件下node將阻塞在這里。
- check階段:執行setImmediate()設定的callbacks。
- close callbacks階段:執行socket.on('close', ....)這些callbacks。
5.2 其中宏隊列有4個,各種類型的任務主要集中在以下四個隊列之中:
- Timers Queue
- IO Callbacks Queue
- Check Queue
- Close Callbacks Queue
微隊列主要有2個,不同的微任務放在不同的微隊列中:
- Next Tick Queue:是放置process.nextTick(callback)的回調任務的
- Other Micro Queue:放置其他microtask,比如Promise等
6. Node的 EventLoop的具體流程:
- 執行全局Script的同步代碼。
- 執行microtask微任務,先執行所有Next Tick Queue中的所有任務,再執行Other Microtask Queue中的所有任務。
- 執行macrotask宏任務,共6個階段,從第1個階段開始執行相應每一個階段macrotask中的所有任務,注意,這里是所有每個階段宏任務隊列的所有任務,在瀏覽器的Event Loop中是只取宏隊列的第一個任務出來執行,每一個階段的macrotask任務執行完畢后,開始執行微任務,也就是步驟2。
- Timers Queue -> 步驟2 -> I/O Queue -> 步驟2 -> Check Queue -> 步驟2 -> Close Callback Queue -> 步驟2 -> Timers Queue ......
- 重復1 - 4過程。
下面做一個小測試吧
console.log(0); setTimeout(() => { // callback1 console.log(1); setTimeout(() => { // callback2 console.log(2); }, 0); setImmediate(() => { // callback3 console.log(3); }) process.nextTick(() => { // callback4 console.log(4); }) }, 0); setImmediate(() => { // callback5 console.log(5); process.nextTick(() => { // callback6 console.log(6); }) }) setTimeout(() => { // callback7 console.log(7); process.nextTick(() => { // callback8 console.log(8); }) }, 0); process.nextTick(() => { // callback9 console.log(9); }) console.log(10);
答案:0, 10, 9, 1, 4, 7, 8, 5, 6, 3, 2 。
7. 總結
1. 事件循環是 瀏覽器 和 Node 執行JS代碼的核心機制,但瀏覽器 和 NodeJS事件循環的實現機制有些不同。
2. 瀏覽器事件循環有一個宏隊列,一個微隊列,且微隊列在執行過程中一個接一個執行一直到隊列為空,宏隊列只取隊首的一個任務放入執行棧執行,執行過后接着執行微隊列,並構成循環。
3. NodeJS事件循環有四個宏隊列,兩個微隊列,微隊列執行方式和瀏覽器的類似,先執行Next Tick Queue所有任務,再執行Other Microtask Queue所有任務。 但宏隊列執行時會依次執行隊列中的每個任務直至隊為空才開始再次執行微隊列任務。
4. MacroTask包括: setTimeout、setInterval、 setImmediate(Node)、requestAnimation(瀏覽器)、IO、UI rendering
5. Microtask包括: process.nextTick(Node)、Promise、Object.observe、MutationObserver
8. 后記
學習事件循環會讓我們對JS引擎執行代碼流程有一個大概的了解,如果遇到任務執行順序帶來的問題,我們也能更快的解決。 同時也會讓我們對異步編程有一個更深的認識。