從第 6 章我們所寫的 server.mjs 程式中,出現了這樣的寫法:
req.on('data', chunk => {
body += chunk;
});這不是平常同步語言中常見的寫法。這段程式代表了一個核心觀念:Node.js 是事件導向(Event-Driven)架構的語言。它不使用傳統的「一行一行、等資料回來再往下執行」的思維,而是透過事件註冊與非同步回呼來處理 I/O。
這樣的設計來自於 Node.js 的兩大特性:
- Event-driven(事件導向)
- Non-blocking I/O(非阻塞輸入輸出)
若要深入理解 Non-blocking I/O,只要從一個日常任務出發就好:檔案讀取。
Node.js 提供 File System 模組(fs)來處理檔案讀取。查閱官方 API 說明文件,我們會找到這個函數:
fs.readFile(filename, [options], callback)這個 callback 就是我們在第 5 章所熟悉的 Lambda(暱名函數)。為什麼讀檔案不直接 return 結果?為什麼要給一個 callback?這就是 Non-blocking I/O 的精髓。
從傳統 C / PHP / Java 的思維來看,初學者常常會寫出這樣的程式結構(虛擬碼):
fs.readFile('hello.txt', data);
console.log(data); // 想像這行可以直接印出結果這是同步式邏輯 —— 呼叫 readFile(),等它回傳結果,再印出。
然而,在 Node.js 中這樣寫會失敗,因為:
Node.js 的 I/O 操作預設是非同步進行的,並不會「等」檔案讀完才執行下一行。
也就是說,當執行 console.log(data) 時,檔案可能尚未讀完,data 仍是 undefined。
import fs from 'fs';
fs.readFile('hello.txt', 'utf8', (err, data) => {
if (err) throw err;
console.log("[DATA]", data);
});當 readFile 讀取完成時,Node.js 會主動「回呼」這個 Lambda 函數,並傳入結果。
let data;
fs.readFile('hello.txt', 'utf8', (err, result) => {
data = result;
});
console.log("[DATA]", data); // 錯誤!此時 data 尚未被賦值這是初學者最容易遇到的問題:尚未理解事件與非同步執行順序的差異。
主程式執行 → 呼叫 readFile()
↓
建立 callback 等待完成
↓
下一行立即繼續執行
↓
(完成後才觸發 callback)
Non-blocking I/O 就是:不要卡住主線程,讓事件完成後通知我。
這樣的設計,讓 Node.js 特別適合高併發、即時應用、網路伺服器等場景。
在下一節,我們將進一步探討:非同步寫法如何進化為 Promise 與 async/await,進而改善 callback 地獄的可讀性與維護性。