[NodeJS] NodeJS運(yùn)行原理簡(jiǎn)記
NodeJS的基本組成
NodeJS是JavaScript運(yùn)行時(shí),主要由V8引擎和libuv組成,其中V8使用 javascript 和 c++ 編寫,而libuv是純 c++ 編寫的,二者都是開源的。

V8引擎用于將 javascript 代碼轉(zhuǎn)換為計(jì)算機(jī)可以執(zhí)行的機(jī)器碼;
而libuv則負(fù)責(zé)完成異步IO、與操作系統(tǒng)交互(文件系統(tǒng)和網(wǎng)絡(luò)模塊)、事件循環(huán)、線程池等等。
Node還有其它模塊:
- http-parser:用于解析http;
- c-ares:用于處理DNS請(qǐng)求;
- OpenSSL:用于加密和安全編程;
- zlib:與壓縮有關(guān)。
總而言之,NodeJS相當(dāng)于Javascript和操作系統(tǒng)之間的一個(gè)抽象層,為開發(fā)人員提供了API,使得開發(fā)人員可以編寫純JavaScript代碼來操控操作系統(tǒng)。
NodeJS的特點(diǎn)
- 單線程,基于事件驅(qū)動(dòng)的非阻塞IO模型,使得NodeJS非常輕量級(jí)和高效;
- 適用于需要快速且可擴(kuò)展的數(shù)據(jù)密集型Web應(yīng)用程序,例如:
- 帶有數(shù)據(jù)庫的API(最好是像MongoDB這樣的NoSQL數(shù)據(jù)庫);
- 數(shù)據(jù)流式傳輸
- 實(shí)時(shí)聊天應(yīng)用
- 服務(wù)端Web應(yīng)用(例如使用Pug這種模板引擎的模式)
- 不適用于CPU密集型的服務(wù)端處理任務(wù),例如圖像處理、視頻轉(zhuǎn)換、文件壓縮等等。
NodeJS程序的工作流程
NodeJS工作在單線程中,在開發(fā)后端服務(wù)的時(shí)候,應(yīng)該時(shí)刻注意不要阻塞這個(gè)線程。
在啟動(dòng)NodeJS進(jìn)程之后,主線程的工作流程如下:
- 初始化程序 initialize program
- 執(zhí)行頂層代碼 execute top-level code
- 獲取依賴模塊 require modules
- 注冊(cè)回調(diào)事件 register event callbacks
- 啟動(dòng)事件循環(huán) start event loop
事件循環(huán)是整個(gè)Node應(yīng)用的核心,只能應(yīng)付簡(jiǎn)單的任務(wù),開銷較大的任務(wù)不能在事件循環(huán)中執(zhí)行,否則會(huì)阻塞主線程(對(duì)于后端服務(wù)來說是致命的)。
開銷較大的任務(wù)實(shí)際上會(huì)被卸載到libuv提供的線程池中執(zhí)行,線程池默認(rèn)的數(shù)量是4,可以配置,至多128個(gè)。(配置項(xiàng)是process.env.UV_THREADPOLL_SIZE)
如何卸載任務(wù)和卸載哪些任務(wù)到線程池是由Node處理的,與開發(fā)人員無關(guān)。會(huì)被卸載到線程池執(zhí)行的任務(wù)是計(jì)算開銷比較大的,比如:
- 文件系統(tǒng)API
- 與密碼學(xué)相關(guān)的API
- 與壓縮有關(guān)的操作
- DNS查詢
事件循環(huán):
Node中的事件循環(huán)分為多個(gè)階段,每個(gè)階段都有對(duì)應(yīng)的回調(diào)隊(duì)列,每次到達(dá)一個(gè)階段,會(huì)執(zhí)行隊(duì)列里的任務(wù)。
當(dāng)隊(duì)列被清空或者執(zhí)行一定數(shù)量的任務(wù)后,則會(huì)進(jìn)入下一個(gè)階段。
具體流程如下:
詳細(xì)的總結(jié)可以看官方文檔:Node.js — The Node.js Event Loop (nodejs.org)
或者可以看我之前寫的一篇博客:[NodeJS] NodeJS事件循環(huán) - feixianxing - 博客園 (cnblogs.com)
對(duì)于剛開始嘗試使用NodeJS開發(fā)后端應(yīng)用的前端開發(fā)人員來說,使用NodeJS編寫服務(wù)端代碼有以下注意事項(xiàng):
-
在回調(diào)函數(shù)中使用
fs、crypto、zlib的API都應(yīng)該使用異步的,因?yàn)檫@時(shí)已經(jīng)進(jìn)入事件循環(huán)階段了,不能阻塞;而在頂層同步代碼的范圍內(nèi),則可以根據(jù)情況選擇同步或者異步API。案例:有一個(gè)接口,它的響應(yīng)與一個(gè)較大的文件有關(guān),而這個(gè)文件的內(nèi)容是不變的。假如每次請(qǐng)求都去異步地讀取這個(gè)文件內(nèi)容,然后再返回的話,其實(shí)很耗費(fèi)時(shí)間,可以考慮在啟動(dòng)服務(wù)器的時(shí)候就同步/異步的讀取文件內(nèi)容保存到一個(gè)變量里,后續(xù)每次請(qǐng)求只需要拿變量的數(shù)據(jù)就OK了(其實(shí)就是做了個(gè)緩存,不需要每次請(qǐng)求都去磁盤找內(nèi)容,在服務(wù)啟動(dòng)的時(shí)候就把內(nèi)容先讀到內(nèi)存了)。
-
不要執(zhí)行復(fù)雜的計(jì)算(即時(shí)間復(fù)雜度高的算法);
-
謹(jǐn)慎處理復(fù)雜對(duì)象的JSON序列化;
-
不要使用復(fù)雜的正則表達(dá)式;
-
將耗時(shí)任務(wù)卸載給線程池或者使用
child.processes;
總結(jié):核心思想就是不要阻塞主線程,因?yàn)閬碜运杏脩舻乃姓?qǐng)求都通過主線程處理,一旦阻塞基本上服務(wù)就廢了。
事件驅(qū)動(dòng)架構(gòu)
NodeJS提供了一個(gè)events模塊,其中有一個(gè)類是EventEmitter,這個(gè)類是NodeJS事件驅(qū)動(dòng)架構(gòu)的核心,很多其它NodeJS的核心類都繼承了這個(gè)類。
http模塊中的 server 就是繼承了EventEmitter,因此有相關(guān)的 on 方法:
const server = http.createServer(); server.on('request', (req, res)=>{ console.log('Request received'); res.end('Request received'); });
EventEmitter是基于發(fā)布/訂閱模式設(shè)計(jì)的:
- 發(fā)布者
Emitters通過emit方法發(fā)布指定名稱的事件; - 訂閱者
Listeners通過on方法訂閱指定名稱的事件并注冊(cè)回調(diào)函數(shù)。
簡(jiǎn)單地介紹發(fā)布/訂閱模式:發(fā)布者和訂閱者之間是松散耦合的,它們互相不知道對(duì)方的存在,僅通過中間的消息代理進(jìn)行通信。
EventEmitter使用指南:
-
限制監(jiān)聽器數(shù)量:一個(gè)事件如果有太多監(jiān)聽器會(huì)占用大量?jī)?nèi)存。可以使用
setMaxListeners方法來控制最大監(jiān)聽器數(shù)量。const EventEmitter = require('events'); const emitter = new EventEmitter(); emitter.setMaxListeners(10); // 設(shè)置最大監(jiān)聽器數(shù)量為10 -
正確處理錯(cuò)誤:始終提供錯(cuò)誤監(jiān)聽器以捕獲和優(yōu)雅地處理錯(cuò)誤;如果沒有監(jiān)聽錯(cuò)誤事件,
emit('error')會(huì)拋出異常,并打印調(diào)用堆棧,結(jié)束進(jìn)程。 -
移除不再使用的監(jiān)聽器:在不再需要時(shí)清理監(jiān)聽器,以釋放資源。
-
使用描述性的事件名稱:使用有意義且描述性的事件名稱,使代碼更加可讀和易于維護(hù)。
參考
[1] B站 NodeJS 教學(xué)視頻
[2] Events | Node.js v22.4.0 Documentation
[3] Node.js——The Node.js Event Loop
[4] [NodeJS] NodeJS事件循環(huán) - feixianxing - 博客園
[5] geeks for geeks: What is EventEmitter in Node.js ?

浙公網(wǎng)安備 33010602011771號(hào)