NodeJS是一個基於V8引擎和libuv的JavaScript運行時,適用於輕量級和高效的數據密集型Web應用。其單線程、非阻塞IO模型依賴事件迴圈和線程池管理非同步任務。使用NodeJS開發需避免阻塞主線程,正確處理事件和錯誤。 ...
NodeJS的基本組成
NodeJS是JavaScript運行時,主要由V8引擎和libuv組成,其中V8使用 javascript 和 c++ 編寫,而libuv是純 c++ 編寫的,二者都是開源的。
V8引擎用於將 javascript 代碼轉換為電腦可以執行的機器碼;
而libuv則負責完成非同步IO、與操作系統交互(文件系統和網路模塊)、事件迴圈、線程池等等。
Node還有其它模塊:
- http-parser:用於解析http;
- c-ares:用於處理DNS請求;
- OpenSSL:用於加密和安全編程;
- zlib:與壓縮有關。
總而言之,NodeJS相當於Javascript和操作系統之間的一個抽象層,為開發人員提供了API,使得開發人員可以編寫純JavaScript代碼來操控操作系統。
NodeJS的特點
- 單線程,基於事件驅動的非阻塞IO模型,使得NodeJS非常輕量級和高效;
- 適用於需要快速且可擴展的數據密集型Web應用程式,例如:
- 帶有資料庫的API(最好是像MongoDB這樣的NoSQL資料庫);
- 數據流式傳輸
- 實時聊天應用
- 服務端Web應用(例如使用Pug這種模板引擎的模式)
- 不適用於CPU密集型的服務端處理任務,例如圖像處理、視頻轉換、文件壓縮等等。
NodeJS程式的工作流程
NodeJS工作在單線程中,在開發後端服務的時候,應該時刻註意不要阻塞這個線程。
在啟動NodeJS進程之後,主線程的工作流程如下:
- 初始化程式 initialize program
- 執行頂層代碼 execute top-level code
- 獲取依賴模塊 require modules
- 註冊回調事件 register event callbacks
- 啟動事件迴圈 start event loop
事件迴圈是整個Node應用的核心,只能應付簡單的任務,開銷較大的任務不能在事件迴圈中執行,否則會阻塞主線程(對於後端服務來說是致命的)。
開銷較大的任務實際上會被卸載到libuv提供的線程池中執行,線程池預設的數量是4,可以配置,至多128個。(配置項是process.env.UV_THREADPOLL_SIZE
)
如何卸載任務和卸載哪些任務到線程池是由Node處理的,與開發人員無關。會被卸載到線程池執行的任務是計算開銷比較大的,比如:
- 文件系統API
- 與密碼學相關的API
- 與壓縮有關的操作
- DNS查詢
事件迴圈:
Node中的事件迴圈分為多個階段,每個階段都有對應的回調隊列,每次到達一個階段,會執行隊列里的任務。
當隊列被清空或者執行一定數量的任務後,則會進入下一個階段。
具體流程如下:
詳細的總結可以看官方文檔:Node.js — The Node.js Event Loop (nodejs.org)
或者可以看我之前寫的一篇博客:[NodeJS] NodeJS事件迴圈 - feixianxing - 博客園 (cnblogs.com)
graph TD; start-->timers; timers==>pending[pending callbacks]; pending==>idle[idle, prepare]; idle==>poll[poll callbacks]; poll==>check; check==>close[close callbacks]; close==>if[Any pending timers or I/O tasks?]; if-->|YES|timers; if-->|NO|exit[exit program]; incoming[incoming: connections, data, etc.]-.->poll對於剛開始嘗試使用NodeJS開發後端應用的前端開發人員來說,使用NodeJS編寫服務端代碼有以下註意事項:
-
在回調函數中使用
fs
、crypto
、zlib
的API都應該使用非同步的,因為這時已經進入事件迴圈階段了,不能阻塞;而在頂層同步代碼的範圍內,則可以根據情況選擇同步或者非同步API。案例:有一個介面,它的響應與一個較大的文件有關,而這個文件的內容是不變的。假如每次請求都去非同步地讀取這個文件內容,然後再返回的話,其實很耗費時間,可以考慮在啟動伺服器的時候就同步/非同步的讀取文件內容保存到一個變數里,後續每次請求只需要拿變數的數據就OK了(其實就是做了個緩存,不需要每次請求都去磁碟找內容,在服務啟動的時候就把內容先讀到記憶體了)。
-
不要執行複雜的計算(即時間複雜度高的演算法);
-
謹慎處理複雜對象的JSON序列化;
-
不要使用複雜的正則表達式;
-
將耗時任務卸載給線程池或者使用
child.processes
;
總結:核心思想就是不要阻塞主線程,因為來自所有用戶的所有請求都通過主線程處理,一旦阻塞基本上服務就廢了。
事件驅動架構
NodeJS提供了一個events
模塊,其中有一個類是EventEmitter
,這個類是NodeJS事件驅動架構的核心,很多其它NodeJS的核心類都繼承了這個類。
http模塊中的 server 就是繼承了EventEmitter,因此有相關的 on 方法:
const server = http.createServer(); server.on('request', (req, res)=>{ console.log('Request received'); res.end('Request received'); });
EventEmitter
是基於發佈/訂閱模式設計的:
- 發佈者
Emitters
通過emit
方法發佈指定名稱的事件; - 訂閱者
Listeners
通過on
方法訂閱指定名稱的事件並註冊回調函數。
簡單地介紹發佈/訂閱模式:發佈者和訂閱者之間是鬆散耦合的,它們互相不知道對方的存在,僅通過中間的消息代理進行通信。
EventEmitter使用指南:
-
限制監聽器數量:一個事件如果有太多監聽器會占用大量記憶體。可以使用
setMaxListeners
方法來控制最大監聽器數量。const EventEmitter = require('events'); const emitter = new EventEmitter(); emitter.setMaxListeners(10); // 設置最大監聽器數量為10
-
正確處理錯誤:始終提供錯誤監聽器以捕獲和優雅地處理錯誤;如果沒有監聽錯誤事件,
emit('error')
會拋出異常,並列印調用堆棧,結束進程。 -
移除不再使用的監聽器:在不再需要時清理監聽器,以釋放資源。
-
使用描述性的事件名稱:使用有意義且描述性的事件名稱,使代碼更加可讀和易於維護。
參考
[1] B站 NodeJS 教學視頻
[2] Events | Node.js v22.4.0 Documentation
[3] Node.js——The Node.js Event Loop
[4] [NodeJS] NodeJS事件迴圈 - feixianxing - 博客園
[5] geeks for geeks: What is EventEmitter in Node.js ?