本頁內容

本概述涵蓋了 Node.js 中阻塞非阻塞呼叫的區別。本概述將提及事件迴圈和 libuv,但不需要事先了解這些主題。假定讀者對 JavaScript 語言和 Node.js 回撥模式有基礎的瞭解。

“I/O” 主要指與系統磁碟和網路的互動,這些互動由 libuv 提供支援。

阻塞是指 Node.js 程序中後續 JavaScript 的執行必須等待非 JavaScript 操作完成。這是因為當阻塞操作發生時,事件迴圈無法繼續執行 JavaScript。

在 Node.js 中,如果 JavaScript 因 CPU 密集型任務(而不是等待非 JavaScript 操作,如 I/O)導致效能不佳,通常不會被稱為阻塞。Node.js 標準庫中使用 libuv 的同步方法是最常用的阻塞操作。原生模組也可能包含阻塞方法。

Node.js 標準庫中的所有 I/O 方法都提供了非同步版本,它們是非阻塞的,並接受回撥函式。一些方法也有阻塞的對應版本,其名稱以 Sync 結尾。

阻塞方法同步執行,非阻塞方法非同步執行。

以檔案系統模組為例,這是一個同步檔案讀取操作:

const fs = require('node:fs');

const data = fs.readFileSync('/file.md'); // blocks here until file is read

以下是對應的非同步示例:

const fs = require('node:fs');

fs.readFile('/file.md', (err, data) => {
  if (err) {
    throw err;
  }
});

第一個示例看起來比第二個簡單,但缺點是第二行會阻塞後續任何 JavaScript 的執行,直到檔案完全讀取完畢。請注意,在同步版本中,如果丟擲錯誤,則需要捕獲它,否則程序將會崩潰。而在非同步版本中,編寫者可以決定是否需要如示例所示那樣丟擲錯誤。

讓我們稍微擴充套件一下示例:

const fs = require('node:fs');

const data = fs.readFileSync('/file.md'); // blocks here until file is read
console.log(data);
moreWork(); // will run after console.log

這裡是一個類似但並不等價的非同步示例:

const fs = require('node:fs');

fs.readFile('/file.md', (err, data) => {
  if (err) {
    throw err;
  }

  console.log(data);
});
moreWork(); // will run before console.log

在上面的第一個示例中,console.log 將在 moreWork() 之前被呼叫。在第二個示例中,fs.readFile()非阻塞的,因此 JavaScript 執行可以繼續,且 moreWork() 會先被呼叫。能夠在不等待檔案讀取完成的情況下執行 moreWork() 是一項關鍵的設計選擇,它允許實現更高的吞吐量。

Node.js 中的 JavaScript 執行是單執行緒的,因此併發是指事件迴圈在完成其他工作後執行 JavaScript 回撥函式的能力。任何預期以併發方式執行的程式碼都必須允許事件迴圈在進行 I/O 等非 JavaScript 操作時繼續執行。

例如,假設 web 伺服器的每個請求需要 50ms 完成,其中 45ms 是可以非同步完成的資料庫 I/O。選擇非阻塞非同步操作可以將每個請求中原本被佔用的 45ms 釋放出來,以處理其他請求。僅透過選擇使用非阻塞方法代替阻塞方法,容量就會有顯著差異。

事件迴圈不同於許多其他語言的模型,在那些語言中,可能會建立額外的執行緒來處理併發工作。

在處理 I/O 時,有一些模式應該避免。讓我們看一個示例:

const fs = require('node:fs');

fs.readFile('/file.md', (err, data) => {
  if (err) {
    throw err;
  }

  console.log(data);
});
fs.unlinkSync('/file.md');

在上面的示例中,fs.unlinkSync() 可能會在 fs.readFile() 之前執行,這將導致 file.md 在被讀取之前就被刪除了。編寫此程式碼的更好方式是完全非阻塞,並保證按正確順序執行:

const fs = require('node:fs');

fs.readFile('/file.md', (readFileErr, data) => {
  if (readFileErr) {
    throw readFileErr;
  }

  console.log(data);

  fs.unlink('/file.md', unlinkErr => {
    if (unlinkErr) {
      throw unlinkErr;
    }
  });
});

上述程式碼在 fs.readFile() 的回撥中放置了一個對 fs.unlink()非阻塞呼叫,這保證了操作的正確順序。