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