在上一節,我們學會了如何非同步讀取單一檔案。但如果任務升級成:「先讀取一個檔案清單,再逐一讀取清單中列出的每個檔案內容」,你會怎麼做?
這正是本節要處理的問題:
- 先讀取
index.txt檔案,其內容為:
chapter1.txt
chapter2.txt
chapter3.txt
chapter4.txt
chapter5.txt
- 接著,逐一讀取這些章節檔案的內容,並印出結果
我們將依照下列步驟完成這項任務,並在每個階段揭露背後潛藏的非同步挑戰,並逐步演化出更好的控制策略。
我們先用傳統方式:先讀取清單,再逐行處理。這裡我們用 buffered-reader 套件讀取 index.txt,並儲存每一行(每個檔案名稱)。
npm install buffered-readerimport { DataReader } from 'buffered-reader';
import fs from 'fs';
const files = [];
const getFileList = (filepath, callback) => {
new DataReader(filepath, { encoding: 'utf8' })
.on('line', (line) => {
files.push(line);
})
.on('end', () => {
callback(files);
})
.read();
};getFileList('index.txt', (fileList) => {
for (let i = 0; i < fileList.length; i++) {
fs.readFile(fileList[i], 'utf8', (err, data) => {
if (err) throw err;
console.log(`[DATA ${i}]`, data);
});
}
});執行時你會發現:
[DATA 4] This is chapter 5.
[DATA 0] This is chapter 1.
[DATA 3] This is chapter 4.
[DATA 2] This is chapter 3.
[DATA 1] This is chapter 2.
問題來了:你無法保證讀取結果的順序。
為什麼?因為 fs.readFile() 是非同步操作,它會同時啟動所有讀檔任務,誰先完成就誰先輸出。
✅ 這正是 callback 無法處理「非同步流程順序控制」的最大弱點。
我們將每次 fs.readFile() 的動作包裝成 Promise 物件,讓每個讀檔變成一個「可以等待」的操作。
const readFileAsync = (filepath) => {
return new Promise((resolve, reject) => {
fs.readFile(filepath, 'utf8', (err, data) => {
if (err) reject(err);
else resolve(data);
});
});
};getFileList('index.txt', async (fileList) => {
const promises = fileList.map((file) => readFileAsync(file));
try {
const contents = await Promise.all(promises);
contents.forEach((content, index) => {
console.log(`[DATA ${index}]`, content);
});
} catch (err) {
console.error('Error reading files:', err);
}
});- 順序一致(Promise.all 回傳的陣列順序對應原始檔案清單)
- 錯誤集中處理(用 try/catch)
- 程式碼層次平坦,不再巢狀
你也可以將整體流程寫成一個 async 函式,讓語法更接近同步邏輯:
const run = async () => {
const fileList = await new Promise((resolve) => {
const list = [];
new DataReader('index.txt', { encoding: 'utf8' })
.on('line', (line) => list.push(line))
.on('end', () => resolve(list))
.read();
});
for (const filename of fileList) {
try {
const data = await readFileAsync(filename);
console.log(`[DATA] ${data}`);
} catch (err) {
console.error('Error reading file:', filename, err);
}
}
};
run();這就是 async/await 的力量:用同步邏輯書寫非同步流程。
| 寫法 | 結構 | 可讀性 | 控制性 | 錯誤處理 | 適合場景 |
|---|---|---|---|---|---|
| callback | 巢狀 | 差 | 差 | 分散 | 簡單非同步任務 |
| Promise | 線性 | 中 | 好 | 集中 | 多任務併發 |
| async/await | 最線性結構 | 最佳 | 最佳 | try/catch | 複雜流程與錯誤控制 |
從這裡開始,我們將逐步搭建起一個能處理多重請求、回應與錯誤的非同步伺服器。
下一節,我們將回到 Web context:如何用非同步方式設計一個 JSON API 回應?