這是一篇翻譯文,原文來自Altamash Ali 於2021年11月14日在Dev.to的文章(原文連結);
又來翻譯有關Node.js的文章,更深原作對於事件迴圈Event loop更深入的說明,期望大家能有所收穫
前一篇文章(連結)以與其原文(連結),說明有關Node.js的核心,以及單一執行緒如何有效處裡多個非同步的事件,也討論到事件迴圈(event loop)工作方式和事件驅動(event-driven)的架構,建議先閱讀前文後再接續這篇文章。
本篇將討論更多迴圈(Event loop)的運作,以及數個範例來觀察各個運作階段。
在開始介紹前,很多人可能會問:為何一位Node.js的開發者需要深入理解事件迴圈(event loop),答案如下:
.事件迴圈(event loop)掌控所有應用程式內的事件排程,任何錯誤的觀念都會導致效能低落,或是寫出充滿bug的程式碼
.而且,在Node.js的後端職缺,是重點且常見的面試問題
所以,讓我們開始吧!
如同先前所述,事件迴圈(event loop)沒有甚麼特別,就是迴圈(loop),一個對一組同步事件分工器(Synchronous event demultiplexer)傳入的事件不斷的迴圈,來觸發迴呼(callback)函式,傳送到應用端。
事件迴圈階段(Eevent Loop Phases)
事件迴圈有幾個不同的階段,每一個階段包含一系列的執行迴呼函式,這些不同階段有個別不同迴呼函式,取決於應用程式的使用方式。
投票(Poll)
.投票階段式執I/O關聯的迴呼函式
.多數的應用程式程式碼在這階段執行
.是Node.js應用程式的執行起點
檢查(Check)
.在這階段,迴呼函式經由setImediate()的執行被觸發
關閉(Close)
.這階段,迴呼函式經由EventEmitter close events被觸發
.例如,當net.Server TCP server關閉時,它發送關閉事件,在這個階段就會被執行
計時器(Timers)
.在這階段,迴呼函數經由setTimeout()或setInterval()的執行被觸發
等候(Pending)
.特定的系統事件在這個階段運行,例如當net.Socket TCP soccer 丟出一個ECONNREFUSED錯誤時
與上述分離的(上述是Macrotask,巨型任務),
還有兩個特別的微型任務(Microtask)隊列(queues),可以在階段執行時加入迴呼函數(有關Macrotask與Microtask可以看這篇說明)
.第一個微型任務隊列(microtask queues)使用process.nextTick()登記加入迴呼函數
.第二個微型任務隊列(microtask queues)處裡被reject或resolve的promises函數
執行優先與排序
.在迴呼函數的巨型任務,優先於迴呼函數的一般階段
.在next tick(process.nextTick是Node.js獨有的)的微型任務隊列,優先於promise的微型任務列隊
.當應用程式開始執行後,事件迴圈也開始運作,且階段一次只處理一個動作,Node.js將迴呼函式加在不同的適當隊列內
.當事件迴圈取到一個階段,將會運行整段階段的迴呼函式,在全部階段內的迴呼函式執行完後,事件迴圈才會進到下一個階段
讓我們看個程式碼範例:
輸出將會是:8, 3, 2, 1, 4, 7, 6, 5
讓我們看看後端是如何運行的:
.程式碼在投票階段(Poll)一行一行的執行
.首先,fs模組的取得
.接著,setImmediate()呼叫,迴呼函式被加入check queue
.接著,promise resolves被加入promise microtask queue
.接著,process.nextTick()運行,其迴呼函式被加入next tick microtask queue
.然後,fs.readFile()告訴Node.js開始讀取檔案,準備好之後,將迴呼函數放到poll qeue
.最後,console.log(8)被呼叫,”8”被輸出在終端機畫面上
在目前階段的堆疊(stack)完成
.現在,有兩個microtask在協商(決定優先序),next tick microtask是最高優先被檢查的(check),其callback (console.log(3))被執行,”3”被輸出在終端機畫面上;在這個範例只有一個next tick microtask queue,然後,promise microtask是下一個被檢查(check)的,所以其callback (console.log(2))被執行,”2”被輸出在終端機畫面上;到這完成兩個microtask,現在投票(poll)接完成;
.現在,事件迴圈進入檢查階段(check phase),這時有console.log(1)被執行,”1”被輸出在終端機畫面上;兩個(nextTick 與 promise)microtask queues是空的,因此檢查階段結束;
.再來的關閉階段(close phase)是空的,因此進入下一個階段,同樣的,計時器階段(timers phase)與等候階段(pending phase)也都是空的,事件迴圈回到最開頭的投票階段(poll phase);
一旦事件迴圈回到投票階段,事件迴圈暫時沒有其他執行項目,因此,基本上會等待到讀取的檔案完成讀取,一旦完成讀取,fs.readFile()內的迴呼函式開始運行
.在fs.readFile內第一行的console.log(4)立即被執行,”4”被輸出在終端機畫面上;
.接著,setTimeout()被呼叫,console.log(5)被排入計時器隊列(timers queue);
.再來,setImmediate()被呼叫,將console.log(6)被加入檢查隊列(check queue);
.最後,process.nextTick()呼叫,將console.log(7)被加入next tick microtask queue;
這投票階段(poll phases)已結束,microtask queue再次協商(決定優先序)
.首先在next tick queue的console.log(7)率先被執行,”7”被輸出在終端機畫面上;
.再來promise queue內是空的,投票階段(poll phase)結束;
.相同的,事件迴圈進入檢查階段(check phase),console.log(6)被執行,”6”被輸出在終端機畫面上,microtask queue是空的,檢查階段(check phase)結束;
.然後,進入關閉階段(close phase),這階段目前是空的,進入下階段;
.最後,計時器階段(timers phase)開始協商(決定優先序),console.log(5)被執行,”5”被輸出在終端機畫面上;
.一旦上述的都完成後,應用程式沒有其他工作要執行,將結束並離開(exit);
如我們所知,Node.js運行環境是單執行緒,在單一的stack上運行太多程式碼,將會延宕事件迴圈,阻擋其他迴呼函式執行;
為避免事件迴圈的乾渴(starving)現象發生,可以停用一些高耗用CPU的多項任務堆疊(stack),
例如你正在執行1000個資料紀錄,可以考慮分成10批次各100個紀錄,
使用setImmediate()在每個批次執行後的末端,單一批次完成後在接續下一批次;
另一個方法是,分叉一個子程序,並將這個高負載的程序轉移過去;
但,千萬不要使用process.nextTick()來執行批次運行,這會造成microtask queue永遠不會空下來,使應用程式卡在這個階段,在卡著處理時,運行環境不會發出任何錯誤(error),而是會保持在一個殭屍狀態,而且占用了整個CPU。
以上是有關事件迴圈的全部。
希望你有感到閱讀的愉快,且有發現一些有用或有趣的事。
參考資料:
1. Distributed Systems with Node.js (Book)