我們已經學習了陣列高階函數的轉換技巧。今天我們要進入前端開發中最關鍵的課題之一:非同步 (Asynchronous) 資料處理。
不論是呼叫 API 撈取資料、讀取檔案還是設定定時器,React 的資料處理(特別是在 useEffect 內部)高度依賴 JavaScript 的 Promise 與 Async/Await 機制。
JavaScript 是單線程 (Single-threaded) 的語言,一次只能做一件事。若發送網路請求時讓主執行緒停下來等待 API 回傳,整個瀏覽器畫面就會陷入凍結(畫面卡死)。
在 Promise 誕生前,非同步高度依賴 Callback Function。當遇到連續的 API 呼叫時,程式碼會演變成維護噩夢:
// 巢狀 Callback Hell
fetchUser(userId, (user) => {
fetchPosts(user.id, (posts) => {
fetchComments(posts[0].id, (comments) => {
// 邏輯層層嵌套,錯誤處理極度困難
});
});
});
ES6 引入了 Promise,它代表一個未來才會得知結果的非同步操作。Promise 內部擁有三個互斥的狀態:
Pending(待定):初始狀態,操作進行中。
Fulfilled(已實現):操作成功,呼叫 resolve(data)。
Rejected(已拒絕):操作失敗,呼叫 reject(error)。
// 使用 Promise 進行鏈式呼叫 (Chaining)
fetchUser(userId)
.then((user) => fetchPosts(user.id))
.then((posts) => fetchComments(posts[0].id))
.catch((error) => console.error('出錯了:', error));
ES8 (ES2017) 推出的 async/await 是 Promise 的語法糖 (Syntactic Sugar),讓你能用看起來像「同步 (Synchronous)」的直覺順序寫非同步程式碼。
核心規則:
async 宣告在函數前,該函數永遠回傳一個 Promise。
await 只能在 async 函數內部使用,它會暫停該函數的執行,直到 Promise 狀態變為 Fulfilled 或 Rejected。
async function getUserData(userId) {
try {
const user = await fetchUser(userId);
const posts = await fetchPosts(user.id);
return posts;
} catch (error) {
// 使用傳統 try...catch 即可優雅捕捉非同步錯誤
console.error('API 請求失敗:', error);
}
}
在 React 中,我們最常在 useEffect 中呼叫非同步 API,但這裡藏著兩個大坑:
// 錯誤寫法:useEffect 不能傳入 async 函數!
useEffect(async () => {
const data = await fetchData();
setData(data);
}, []);
原因:useEffect 規範 callback 必須回傳 undefined 或 Cleanup Function (清除函數)。但 async 函數永遠會回傳一個 Promise,這會破壞 React 的 Cleanup 機制。
正確做法:在內部宣告獨立的 async 函數並呼叫它
useEffect(() => {
// 在內部定義 async 函數
const loadData = async () => {
try {
const data = await fetchData();
setData(data);
} catch (err) {
setError(err.message);
}
};
loadData(); // 執行它
}, []);
若有多個互相獨立的 API 請求,切勿串行( sequential)使用 await,否則會造成不必要的等待累積:
// 慢:串行等待(總耗時 = 2秒 + 3秒 = 5秒)
const user = await fetchUser(); // 耗時 2 秒
const config = await fetchConfig(); // 耗時 3 秒
// 快:平行發送(總耗時 = 取最大值 3秒)
const [user, config] = await Promise.all([
fetchUser(),
fetchConfig()
]);