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',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 。

誤解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繫結的操作(例如視訊編碼等),開發支援將隨著時間的推移而成熟。

以上就是本文的全部內容,希望對大家的學習有所幫助,也希望大家多多支援我們。