1. 程式人生 > 實用技巧 >關於NodeJS工作原理的五個誤解

關於NodeJS工作原理的五個誤解

Nodejs誕生於2009年,由於它使用了JavaScript,在這些年裡獲得了非常廣泛的流行。它是一個用於編寫伺服器端應用程式的JavaScript執行時,但是"它就是JavaScript"這句話並不是100%正確的。

JavaScript是單執行緒的,它不是被設計用來實現要求可伸縮性的伺服器端上執行的。藉助Google Chrome的高效能V8 JavaScript引擎,libuv的超酷非同步I/O實現以及其他一些刺激性的補充,Nodejs能夠將客戶端JavaScript引入伺服器端,從而能夠編寫超快速的、能夠處理成千上萬的套接字連線的Web JavaScript伺服器。

NodeJS是一個由大量有趣的基礎模組構建的大型平臺。但是,由於對NodeJS的這些內部元件的工作方式缺乏瞭解,因此許多NodeJS開發人員對NodeJS的行為做出了錯誤的理解,並開發了導致嚴重效能問題以及難以跟蹤的錯誤的應用程式。在本文中,我將描述在許多NodeJS開發人員中很常見的五個錯誤理解。

誤解1 — EventEmitter 和事件迴圈相關

編寫NodeJS應用程式時會大量使用NodeJS EventEmitter,但是人們誤認為EventEmitter與NodeJS Event Loop有關,這是不正確的。

NodeJS事件迴圈是NodeJS的核心,它為NodeJS提供了非同步的,非阻塞的I/O機制。它以特定順序處理來自不同型別的非同步事件的完成事件。

相反,NodeJS Event Emitter是一個核心的NodeJS API,它允許你將監聽器函式附加到一個特定的事件,這個事件一旦觸發就會被呼叫。這種行為看起來像是非同步的,因為事件處理程式的呼叫時間通常比它最初作為事件處理程式註冊的時間晚。

EventEmitter例項跟蹤與EventEmitter例項本身內的事件相關聯的所有事件和其例項本身。它不會在事件迴圈佇列中排程任何事件。儲存此資訊的資料結構只是一個普通的老式JavaScript物件,其中物件屬性是事件名稱,屬性的值是一個偵聽器函式或偵聽器函式陣列。

當在EventEmitter例項上呼叫emit函式時,emitter將按順序依次同步調所有註冊到示例上的回撥函式。

看以下程式碼片段:

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同步執行所有事件處理函式,因此I am the last log line在呼叫所有監聽函式完成之後才會列印。

誤解2 - 所有接受回撥的函式都是非同步的

函式是同步的還是非同步的取決於函式在執行期間是否建立非同步資源。根據這個定義,如果給你一個函式,你可以確定給定的函式是非同步的:

  • JavaScript
    NodeJS
    setTimeout,setInterval,setImmediate,process.nextTick
    
  • NodeJS API
    child_process,fs,net
    PromiseAPI
    async-await
    
  • 從C++外掛呼叫一個函式,該函式被編寫為非同步函式(例如bcrypt)

接受回撥函式作為引數不會使函式非同步。但是,通常非同步函式的確接受回撥作為最後一個引數(除非包裝返回一個Promise)。接受回撥並將結果傳遞給回撥的這種模式稱為Continuation Passing Style。你仍然可以使用Continuation Passing Style編寫同步功能。

const sum = (a, b, callback) => {
  callback(a + b);
};

sum(1,2, (result) => {
  console.log(result);
});

同步函式和非同步函式在執行期間在如何使用堆疊方面有很大的不同。同步函式在執行的整個過程中都會佔用堆疊,方法是禁止其他任何人佔用堆疊直到return 為止。相反,非同步函式排程一些非同步任務並立即返回,因此將自身從堆疊中刪除。一旦預定的非同步任務完成,將呼叫提供的任何回撥,並且該回調函式將再次佔據該堆疊。此時,啟動非同步任務的函式將不再可用,因為它已經返回。

考慮到以上定義,請嘗試確定以下函式是非同步還是同步。

function writeToMyFile(data, callback) {
    if (!data) {
        callback(new Error('No data provided'));
    } else {
        fs.writeFile('myfile.txt', data, callback);
    }
}

實際上,上述函式可以是同步的,也可以是非同步的,具體取決於傳遞給的值data。

如果data為 false,callback則將立即呼叫,並出現錯誤。在此執行路徑中,該功能是100%同步的,因為它不執行任何非同步任務。

如果data是 true ,它會將data寫入myfile.txt,將呼叫回撥完成的檔案I/O操作之後。由於非同步檔案I/O操作,此執行路徑是100%非同步的。

強烈建議不要以這種不一致的方式(在此功能同時執行同步和非同步操作)編寫函式,因為這會使應用程式的行為無法預測。幸運的是,這些不一致可以很容易地修復如下:

function writeToMyFile(data, callback) {
    if (!data) {
        process.nextTick(() => callback(new Error('No data provided')));
    } else {
        fs.writeFile('myfile.txt', data, callback);
    }
}

process.nextTick可以用來延遲callback函式的呼叫,從而使執行路徑非同步。

或者,你可以使用 setImmediate 代替 process.nextTick ,這或多或少會產生相同的結果。但是,process.nextTick相對而言,回撥具有更高的優先順序,從而使其比 setImmediate 更快。

誤解3 - 所有佔用大量CPU的功能都在阻止事件迴圈

眾所周知,CPU密集型操作會阻塞Node.js事件迴圈。儘管這句話在一定程度上是正確的,但並不是100%正確,因為有些CPU密集型函式不會阻塞事件迴圈。

一般來說,加密操作和壓縮操作是受CPU高度限制的。由於這個原因,某些加密函式和zlib函式的非同步版本以在libuv執行緒池上執行計算的方式編寫,這樣它們就不會阻塞事件迴圈。其中一些功能是:

  • crypto.pbkdf2()
  • crypto.randomFill()
  • crypto.randomBytes()
  • 所有zlib非同步功能

但是,在撰寫本文時,還無法使用純JavaScript在libuv執行緒池上執行CPU密集型操作。但是,你可以編寫自己的C++外掛,使你能夠安排libuv執行緒池上的工作。有某些第三方庫(例如bcrypt),它們執行CPU密集型操作並使用C++外掛來實現針對CPU繫結操作的非同步API。

誤解4 - 所有非同步操作都線上程池上執行

現代作業系統具有內建的核心支援,可使用事件通知(例如,Linux中的epoll,macOS中的kqueue,Windows中的IOCP等)以有效的方式促進網路I/O操作的本機非同步。因此,不會在libuv執行緒池上執行網路I/O。

但是,當涉及到檔案I/O時,跨作業系統以及同一作業系統中的某些情況存在許多不一致之處。這使得為檔案I/O實現通用的獨立於平臺的API極為困難。因此,在libuv執行緒池上執行檔案系統操作以公開一致的非同步API。

dns.lookup() dns模組中的函式是另一個利用libuv執行緒池的API。原因是,使用dns.lookup()功能將域名解析為IP地址是與平臺有關的操作,並且此操作不是100%的網路I/O。

豌豆資源搜尋網站https://55wd.com 電腦刺繡繡花廠 ttp://www.szhdn.com

誤解5 - 不應使用NodeJS編寫CPU密集型應用程式

這並不是真正的誤解,而是關於NodeJS的一個眾所周知的事實,現在由於在Node v10.5.0中引入Worker Threads而被淘汰了。儘管它是作為實驗性功能引入的,但worker_threads自Node v12 LTS起,該模組現已穩定,因此適合在具有CPU密集型操作的生產應用程式中使用。

每個Node.js工作執行緒將擁有其自己的v8執行時的副本,事件迴圈和libuv執行緒池。因此,執行阻塞CPU密集型操作的一個工作執行緒不會影響其他工作執行緒的事件迴圈,從而使它們可用於任何傳入的工作。

但是,在撰寫本文時,IDE對Worker Threads的支援還不是最大。某些IDE不支援將偵錯程式附加到在主執行緒以外的其他執行緒中執行的程式碼。但是,隨著許多開發人員已經開始採用輔助執行緒進行CPU繫結的操作(例如視訊編碼等),開發支援將隨著時間的推移而成熟。