Joseph Chen
一、非同步的起點:Callback 與 Callback Hell
JS 最早的非同步寫法是回呼函式(Callback)——把「完成後要做的事」作為參數傳進去, 讓非同步操作完成後呼叫它。但連續的非同步操作會讓 callback 一層套一層,變成難以維護的「Callback Hell」。
1. 可讀性差:越來越深的縮排讓程式碼難以閱讀。
2. 錯誤處理重複:每一層都要檢查 err。
3. 難以維護:邏輯順序和視覺順序不一致,修改容易出錯。
二、Promise:承諾「這個值未來會到」
Promise 代表一個尚未完成但最終會完成(或失敗)的操作。 它有三種狀態,且狀態只能單向轉換:
Pending
等待中,初始狀態
Settled
已 resolve,有結果值
Pending
等待中
Rejected
已失敗,有錯誤
Promise 的基本寫法
用 Promise 解決 Callback Hell
Promise 的靜態方法
Promise.all()並行執行多個 Promise,全部 resolve 才 resolve,任一 reject 就立刻 reject。
Promise.allSettled()等所有 Promise 都 settle(無論成功失敗),回傳每個的狀態和結果。適合「失敗也要繼續」的場景。
Promise.race()回傳第一個 settle 的結果,常用於超時控制。
三、async/await:讓非同步看起來像同步
async/await 是 ES2017 引入的語法糖, 底層仍是 Promise,但讓非同步程式碼的寫法和讀起來的感覺像同步一樣直觀。
async/await vs Promise.then 對照
function loadData(id) {
return fetchUser(id)
.then(user => {
return fetchOrders(user.id)
.then(orders => ({
user,
orders,
}));
})
.catch(err => {
console.error(err);
});
}async function loadData(id) {
try {
const user = await fetchUser(id);
const orders = await fetchOrders(
user.id
);
return { user, orders };
} catch (err) {
console.error(err);
}
}四、async/await 最常見的陷阱
在迴圈裡 await,變成依序執行
用 for...of 迴圈 + await 是依序執行的—— 等第一個完成才執行第二個,總時間是全部加總。 如果每個任務互相獨立,應該並行執行。
// 假設每個 fetch 需要 1 秒
// 這樣總共要等 3 秒
async function sequential(ids) {
const results = [];
for (const id of ids) {
const data = await fetch(id); // 等這個完成才繼續
results.push(data);
}
return results;
}// 同時發出三個請求,等最慢的那個
// 總共只要 1 秒(最慢的那個)
async function parallel(ids) {
const promises = ids.map(id => fetch(id));
const results = await Promise.all(promises);
return results;
}獨立的非同步任務應該用 Promise.all 並行執行。只有當後面的任務依賴前面的結果時,才需要 await 依序執行。
忘記 await 導致拿到 Promise 物件而非值
async function getUser() {
return fetchUser(1); // Promise,不是 User
}
async function main() {
const user = getUser(); // 又忘了 await
console.log(user.name); // undefined!
// user 是 Promise 物件,不是 user 資料
}async function getUser() {
return await fetchUser(1); // User 物件
}
async function main() {
const user = await getUser(); // 等待解析
console.log(user.name); // 'Joseph' ✅
}async 函式的回傳值是 Promise,呼叫時也需要 await。一個常見 bug 是呼叫 async 函式忘記 await,拿到的是 Promise 物件而不是實際的值。
未處理的 Promise rejection
如果 Promise reject 了卻沒有 .catch() 或 try/catch, 在 Node.js 中可能導致程式崩潰(現代版本),在瀏覽器中會產生 UnhandledPromiseRejection 警告。
// 如果 fetchData reject,沒有任何處理
async function main() {
const data = await fetchData(); // 可能 throw
console.log(data);
}
main(); // UnhandledPromiseRejection!async function main() {
try {
const data = await fetchData();
console.log(data);
} catch (err) {
console.error('Error:', err.message);
// 可以 fallback、retry 或顯示錯誤訊息
}
}
main();每個 async 函式的呼叫處都應該有 try/catch,或者在呼叫時加 .catch()。Promise.all 只要有一個 reject 就整個失敗,考慮用 Promise.allSettled 代替。
async 函式不能用在 forEach
Array.forEach 不會等待 async callback, 所有 callback 會幾乎同時啟動,但 forEach 本身不等待它們完成就結束了。
async function processAll(items) {
// forEach 不等待 async callback!
items.forEach(async (item) => {
await processItem(item);
});
// 這行在所有 processItem 完成前就執行了
console.log('全部完成');
}// 依序執行:for...of + await
async function processAll(items) {
for (const item of items) {
await processItem(item);
}
console.log('全部完成');
}
// 並行執行:Promise.all
async function processAllParallel(items) {
await Promise.all(items.map(processItem));
console.log('全部完成');
}forEach 不支援非同步等待。需要依序處理就用 for...of,需要並行處理就用 Promise.all + .map()。
五、核心重點整理
Callback Hell深層巢狀讓程式碼難以閱讀和維護,是 Promise 出現的動機
Promise 狀態Pending → Fulfilled 或 Rejected,狀態不可逆
.then() 鏈每個 then 回傳新 Promise,可以傳遞值;.catch() 統一處理錯誤
Promise.all並行執行,全部成功才成功;任一失敗就失敗
Promise.allSettled並行執行,等全部 settle,不管成敗都繼續
async 函式永遠回傳 Promise,函式體內可以用 await
await暫停 async 函式,等 Promise resolve,拿到結果值
forEach + async不等待,改用 for...of(依序)或 Promise.all(並行)
忘記 await拿到 Promise 物件而非值,是最常見的 async bug