NodeJS開發(fā)人員常見五個(gè)錯(cuò)誤理解
Nodejs 誕生于 2009 年,由于它使用了 JavaScript ,在這些年里獲得了非常廣泛的流行。它是一個(gè)用于編寫服務(wù)器端應(yīng)用程序的 JavaScript 運(yùn)行時(shí),但是 "它就是JavaScript" 這句話并不是 100% 正確的。
JavaScript 是單線程的,它不是被設(shè)計(jì)用來實(shí)現(xiàn)要求可伸縮性的服務(wù)器端上運(yùn)行的。借助 Google Chrome 的高性能 V8 JavaScript 引擎, libuv 的超酷異步 I/O 實(shí)現(xiàn)以及其他一些刺激性的補(bǔ)充, Nodejs 能夠?qū)⒖蛻舳?JavaScript 引入服務(wù)器端,從而能夠編寫超快速的、能夠處理成千上萬的套接字連接的 Web JavaScript 服務(wù)器。
NodeJS 是一個(gè)由大量有趣的基礎(chǔ)模塊構(gòu)建的大型平臺(tái)。但是,由于對 NodeJS 的這些內(nèi)部組件的工作方式缺乏了解,因此許多 NodeJS 開發(fā)人員對 NodeJS 的行為做出了錯(cuò)誤的理解,并開發(fā)了導(dǎo)致嚴(yán)重性能問題以及難以跟蹤的錯(cuò)誤的應(yīng)用程序。在本文中,我將描述在許多 NodeJS 開發(fā)人員中很常見的五個(gè)錯(cuò)誤理解。
誤解1 — EventEmitter 和事件循環(huán)相關(guān)
編寫 NodeJS 應(yīng)用程序時(shí)會(huì)大量使用 NodeJS EventEmitter ,但是人們誤認(rèn)為 EventEmitter 與 NodeJS Event Loop 有關(guān),這是不正確的。
NodeJS 事件循環(huán)是 NodeJS 的核心,它為 NodeJS 提供了異步的,非阻塞的 I/O 機(jī)制。它以特定順序處理來自不同類型的異步事件的完成事件。
相反, NodeJS Event Emitter 是一個(gè)核心的 NodeJS API ,它允許你將監(jiān)聽器函數(shù)附加到一個(gè)特定的事件,這個(gè)事件一旦觸發(fā)就會(huì)被調(diào)用。這種行為看起來像是異步的,因?yàn)槭录幚沓绦虻恼{(diào)用時(shí)間通常比它最初作為事件處理程序注冊的時(shí)間晚。
EventEmitter 實(shí)例跟蹤與 EventEmitter 實(shí)例本身內(nèi)的事件相關(guān)聯(lián)的所有事件和其實(shí)例本身。它不會(huì)在事件循環(huán)隊(duì)列中調(diào)度任何事件。存儲(chǔ)此信息的數(shù)據(jù)結(jié)構(gòu)只是一個(gè)普通的老式 JavaScript 對象,其中對象屬性是事件名稱,屬性的值是一個(gè)偵聽器函數(shù)或偵聽器函數(shù)數(shù)組。
當(dāng)在 EventEmitter 實(shí)例上調(diào)用 emit 函數(shù)時(shí), emitter 將按順序依次同步調(diào)所有注冊到示例上的回調(diào)函數(shù)。
看以下代碼片段:
const EventEmitter = require('events');
const myEmitter = new EventEmitter();
myEmitter.on('myevent', () => console.log('handler1: myevent was fired!'));
myEmitter.on('myevent', () => console.log('handler2: myevent was fired!'));
myEmitter.on('myevent', () => console.log('handler3: myevent was fired!'));myEmitter.emit('myevent');
console.log('I am the last log line');
以上代碼段的輸出為:
handler1: myevent was fired!
handler2: myevent was fired!
handler3: myevent was fired!
I am the last log line
由于 event emitter 同步執(zhí)行所有事件處理函數(shù),因此 I am the last log line 在調(diào)用所有監(jiān)聽函數(shù)完成之后才會(huì)打印。
誤解2 - 所有接受回調(diào)的函數(shù)都是異步的
函數(shù)是同步的還是異步的取決于函數(shù)在執(zhí)行期間是否創(chuàng)建異步資源。根據(jù)這個(gè)定義,如果給你一個(gè)函數(shù),你可以確定給定的函數(shù)是異步的:
JavaScript
NodeJS
setTimeout,setInterval,setImmediate,process.nextTick
NodeJS API
child_process,fs,net
PromiseAPI
async-await
從 C++ 插件調(diào)用一個(gè)函數(shù),該函數(shù)被編寫為異步函數(shù)(例如 bcrypt )
接受回調(diào)函數(shù)作為參數(shù)不會(huì)使函數(shù)異步。但是,通常異步函數(shù)的確接受回調(diào)作為最后一個(gè)參數(shù)(除非包裝返回一個(gè) Promise )。接受回調(diào)并將結(jié)果傳遞給回調(diào)的這種模式稱為 Continuation Passing Style 。你仍然可以使用 Continuation Passing Style 編寫同步功能。
const sum = (a, b, callback) => { callback(a + b); }; sum(1,2, (result) => { console.log(result); });
同步函數(shù)和異步函數(shù)在執(zhí)行期間在如何使用堆棧方面有很大的不同。同步函數(shù)在執(zhí)行的整個(gè)過程中都會(huì)占用堆棧,方法是禁止其他任何人占用堆棧直到return 為止。相反,異步函數(shù)調(diào)度一些異步任務(wù)并立即返回,因此將自身從堆棧中刪除。一旦預(yù)定的異步任務(wù)完成,將調(diào)用提供的任何回調(diào),并且該回調(diào)函數(shù)將再次占據(jù)該堆棧。此時(shí),啟動(dòng)異步任務(wù)的函數(shù)將不再可用,因?yàn)樗呀?jīng)返回。
考慮到以上定義,請嘗試確定以下函數(shù)是異步還是同步。
function writeToMyFile(data, callback) { if (!data) { callback(new Error('No data provided')); } else { fs.writeFile('myfile.txt', data, callback); } }
實(shí)際上,上述函數(shù)可以是同步的,也可以是異步的,具體取決于傳遞給的值 data 。
如果 data 為 false, callback 則將立即調(diào)用,并出現(xiàn)錯(cuò)誤。在此執(zhí)行路徑中,該功能是 100% 同步的,因?yàn)樗粓?zhí)行任何異步任務(wù)。
如果 data 是 true ,它會(huì)將 data 寫入 myfile.txt ,將調(diào)用回調(diào)完成的文件 I/O 操作之后。由于異步文件 I/O 操作,此執(zhí)行路徑是100%異步的。
強(qiáng)烈建議不要以這種不一致的方式(在此功能同時(shí)執(zhí)行同步和異步操作)編寫函數(shù),因?yàn)檫@會(huì)使應(yīng)用程序的行為無法預(yù)測。幸運(yùn)的是,這些不一致可以很容易地修復(fù)如下:
function writeToMyFile(data, callback) { if (!data) { process.nextTick(() => callback(new Error('No data provided'))); } else { fs.writeFile('myfile.txt', data, callback); } }
process.nextTick 可以用來延遲 callback 函數(shù)的調(diào)用,從而使執(zhí)行路徑異步。
或者,你可以使用 setImmediate 代替 process.nextTick ,這或多或少會(huì)產(chǎn)生相同的結(jié)果。但是,process.nextTick相對而言,回調(diào)具有更高的優(yōu)先級(jí),從而使其比 setImmediate 更快。
誤解3 - 所有占用大量CPU的功能都在阻止事件循環(huán)
眾所周知, CPU 密集型操作會(huì)阻塞 Node.js 事件循環(huán)。盡管這句話在一定程度上是正確的,但并不是100%正確,因?yàn)橛行?CPU 密集型函數(shù)不會(huì)阻塞事件循環(huán)。
一般來說,加密操作和壓縮操作是受 CPU 高度限制的。由于這個(gè)原因,某些加密函數(shù)和 zlib 函數(shù)的異步版本以在 libuv 線程池上執(zhí)行計(jì)算的方式編寫,這樣它們就不會(huì)阻塞事件循環(huán)。其中一些功能是:
- crypto.pbkdf2()
- crypto.randomFill()
- crypto.randomBytes()
- 所有 zlib 異步功能
但是,在撰寫本文時(shí),還無法使用純 JavaScript 在 libuv 線程池上運(yùn)行CPU密集型操作。但是,你可以編寫自己的 C++ 插件,使你能夠安排 libuv 線程池上的工作。有某些第三方庫(例如 bcrypt ),它們執(zhí)行CPU密集型操作并使用 C++ 插件來實(shí)現(xiàn)針對CPU綁定操作的異步API。
誤解4 - 所有異步操作都在線程池上執(zhí)行
現(xiàn)代操作系統(tǒng)具有內(nèi)置的內(nèi)核支持,可使用事件通知(例如, Linux 中的 epoll , macOS 中的 kqueue , Windows 中的 IOCP 等)以有效的方式促進(jìn)網(wǎng)絡(luò) I/O 操作的本機(jī)異步。因此,不會(huì)在 libuv 線程池上執(zhí)行網(wǎng)絡(luò) I/O 。
但是,當(dāng)涉及到文件 I/O 時(shí),跨操作系統(tǒng)以及同一操作系統(tǒng)中的某些情況存在許多不一致之處。這使得為文件 I/O 實(shí)現(xiàn)通用的獨(dú)立于平臺(tái)的 API 極為困難。因此,在 libuv 線程池上執(zhí)行文件系統(tǒng)操作以公開一致的異步 API 。
dns.lookup() dns 模塊中的函數(shù)是另一個(gè)利用 libuv 線程池的API。原因是,使用 dns.lookup() 功能將域名解析為IP地址是與平臺(tái)有關(guān)的操作,并且此操作不是 100% 的網(wǎng)絡(luò) I/O 。
誤解5 - 不應(yīng)使用NodeJS編寫CPU密集型應(yīng)用程序
這并不是真正的誤解,而是關(guān)于 NodeJS 的一個(gè)眾所周知的事實(shí),現(xiàn)在由于在 Node v10.5.0 中引入 Worker Threads 而被淘汰了。盡管它是作為實(shí)驗(yàn)性功能引入的,但 worker_threads 自 Node v12 LTS 起,該模塊現(xiàn)已穩(wěn)定,因此適合在具有CPU密集型操作的生產(chǎn)應(yīng)用程序中使用。
每個(gè) Node.js 工作線程將擁有其自己的v8運(yùn)行時(shí)的副本,事件循環(huán)和 libuv 線程池。因此,執(zhí)行阻塞CPU密集型操作的一個(gè)工作線程不會(huì)影響其他工作線程的事件循環(huán),從而使它們可用于任何傳入的工作。
但是,在撰寫本文時(shí),IDE對 Worker Threads 的支持還不是最大。某些IDE不支持將調(diào)試器附加到在主線程以外的其他線程中運(yùn)行的代碼。但是,隨著許多開發(fā)人員已經(jīng)開始采用輔助線程進(jìn)行CPU綁定的操作(例如視頻編碼等),開發(fā)支持將隨著時(shí)間的推移而成熟。
以上就是本文的全部內(nèi)容,希望對大家的學(xué)習(xí)有所幫助,也希望大家多多支持腳本之家。
相關(guān)文章
Node服務(wù)端實(shí)戰(zhàn)之操作數(shù)據(jù)庫示例詳解
這篇文章主要為大家介紹了Node服務(wù)端實(shí)戰(zhàn)之操作數(shù)據(jù)庫示例詳解,有需要的朋友可以借鑒參考下,希望能夠有所幫助,祝大家多多進(jìn)步,早日升職加薪2022-12-12NodeJS Web應(yīng)用監(jiān)聽sock文件實(shí)例
這篇文章主要介紹了NodeJS Web應(yīng)用監(jiān)聽sock文件實(shí)例,本文講解 NodeJS 的 TCP 和 HTTP 監(jiān)聽 Domain Socket 文件例子,需要的朋友可以參考下2015-02-02npm?install常見報(bào)錯(cuò)以及問題詳解
npm?install總是一言難盡,下面這篇文章主要給大家介紹了關(guān)于npm?install常見報(bào)錯(cuò)以及問題的相關(guān)資料,文中通過圖文以及實(shí)例代碼介紹的非常詳細(xì),需要的朋友可以參考下2023-02-02node.js express捕獲全局異常的三種方法實(shí)例分析
這篇文章主要介紹了node.js express捕獲全局異常的三種方法,結(jié)合實(shí)例形式簡單分析了node.js express捕獲全局異常的常見操作方法與使用注意事項(xiàng),需要的朋友可以參考下2019-12-12輕松創(chuàng)建nodejs服務(wù)器(10):處理POST請求
這篇文章主要介紹了輕松創(chuàng)建nodejs服務(wù)器(10):處理POST請求,本文告訴你如何實(shí)現(xiàn)在node.js中處理POST請求,需要的朋友可以參考下2014-12-12npm?install總是卡住不動(dòng)問題的解決辦法
在我們安裝完Node.js之后,需要使用npm命令來安裝一些工具,下面這篇文章主要給大家介紹了關(guān)于npm?install總是卡住不動(dòng)問題的解決辦法,需要的朋友可以參考下2022-05-05package.json的版本號(hào)更新優(yōu)化方法
這篇文章主要為大家介紹了package.json的版本號(hào)更新優(yōu)化方法詳解,有需要的朋友可以借鑒參考下,希望能夠有所幫助,祝大家多多進(jìn)步,早日升職加薪2023-04-04