非同步程式設計 (Async Programming)#
Node.js 是單執行緒的執行環境,透過事件迴圈(Event Loop) 機制,讓程式在等待耗時操作(讀取檔案、網路請求、資料庫查詢)時,不會阻塞其他程式碼的執行。
Promise、async/await 的基礎概念請參考 JavaScript CH08:非同步程式設計。本篇著重於 Node.js 特有的部分。
1. 同步 vs 非同步#
同步:碰到耗時操作,整個程式都要等。
非同步:遇到耗時操作時,先繼續執行後面的程式碼,等結果回來再處理。
1
2
3
4
5
6
| console.log("開始");
setTimeout(() => {
console.log("延遲執行");
}, 1000);
console.log("結束");
// 輸出:開始 → 結束 → (1 秒後)延遲執行
|
2. 回呼函式與 Error-First 規範#
Node.js 內建模組的非同步 API 統一採用 Error-First Callback 規範:第一個參數是錯誤(err),第二個參數才是結果。操作成功時 err 為 null:
1
2
3
4
5
6
7
8
9
10
11
| const fs = require("fs");
fs.readFile("data.txt", "utf8", (err, data) => {
if (err) {
console.error("讀取失敗:", err.message);
return; // 一定要 return,避免繼續執行
}
console.log("檔案內容:", data);
});
console.log("繼續執行其他程式碼..."); // 不等檔案讀取,立刻執行
|
多層巢狀時會產生回呼地獄(Callback Hell):
1
2
3
4
5
6
7
8
9
| fs.readFile("config.json", "utf8", (err, config) => {
db.connect(config, (err, connection) => {
connection.query("SELECT * FROM users", (err, users) => {
fs.writeFile("log.txt", JSON.stringify(users), (err) => {
console.log("完成"); // 程式碼往右無限縮排
});
});
});
});
|
這是 Promise 和 async/await 出現的原因。
3. fs.promises — 內建模組的 Promise 版本#
Node.js 內建模組提供了 Promise 版本,可以直接搭配 async/await 使用,不需要再處理 callback:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
| const fs = require("fs").promises;
// 或
const fs = require("fs/promises"); // Node.js 14+
async function processFile() {
try {
// 直接 await,不需要 callback
const data = await fs.readFile("data.txt", "utf8");
const processed = data.toUpperCase();
await fs.writeFile("output.txt", processed);
console.log("處理完成");
} catch (err) {
console.error("發生錯誤:", err.message);
}
}
|
與 callback 版本對比:
1
2
3
4
5
6
7
8
9
10
11
12
13
| // callback 版本(舊)
fs.readFile("data.txt", "utf8", (err, data) => {
if (err) { ... }
fs.writeFile("output.txt", data.toUpperCase(), (err) => {
if (err) { ... }
console.log("完成");
});
});
// Promise 版本(新)
const data = await fs.readFile("data.txt", "utf8");
await fs.writeFile("output.txt", data.toUpperCase());
console.log("完成");
|
4. 平行執行(Promise.all)#
多個非同步操作若彼此沒有依賴關係,應同時發出,再一起等待:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
| const fs = require("fs/promises");
// 錯誤做法:依序讀取(慢)
async function sequential() {
const a = await fs.readFile("a.txt", "utf8"); // 等完才讀下一個
const b = await fs.readFile("b.txt", "utf8");
const c = await fs.readFile("c.txt", "utf8");
}
// 正確做法:同時讀取(快)
async function parallel() {
const [a, b, c] = await Promise.all([
fs.readFile("a.txt", "utf8"),
fs.readFile("b.txt", "utf8"),
fs.readFile("c.txt", "utf8")
]);
console.log(a, b, c);
}
|
若需要每個都有結果、不因單一失敗而中止,改用 Promise.allSettled:
1
2
3
4
5
6
7
8
9
10
| const results = await Promise.allSettled([
fs.readFile("a.txt", "utf8"),
fs.readFile("not-exist.txt", "utf8"), // 這個會失敗
fs.readFile("c.txt", "utf8")
]);
results.forEach((r, i) => {
if (r.status === "fulfilled") console.log(`檔案 ${i}:`, r.value);
else console.error(`檔案 ${i} 失敗:`, r.reason.message);
});
|
Reference#