在 JavaScript 開發中,異步操作就像家常便飯 —— 從調用後端 API 到讀取本地文件,幾乎無處不在。但很多開發者都會困惑:到底該用 Promise 的鏈式調用,還是 async/await 語法?其實答案很簡單:沒有絕對的好壞,只有場景的適配。
今天我們就用實際案例聊聊,這兩種異步寫法各自適合什麼場景,以及如何在項目中混搭使用,讓代碼既高效又易讀。
先搞懂:兩者不是對立關係
很多人以為 async/await 是 Promise 的替代品,其實大錯特錯。async/await 本質是 Promise 的語法糖,它的底層依然是 Promise 實現。就像用for...of遍歷數組比forEach更直觀一樣,async/await 讓異步代碼看起來更像同步代碼。
先看個最簡單的對比:
// Promise寫法
fetchData().then(data => {
return processData(data);
}).then(result => {
console.log(result);
}).catch(err => {
console.error(err);
});
// async/await寫法
async function handleData() {
try {
const data = await fetchData();
const result = await processData(data);
console.log(result);
} catch (err) {
console.error(err);
}
}
兩者功能完全一致,但 async/await 的線性結構更符合人類的閲讀習慣 —— 這也是它被廣泛採用的核心原因。
優先用 async/await 的 3 種場景
什麼時候用 async/await 更合適?記住一個原則:當異步操作需要按順序執行,或者邏輯中有較多條件判斷時。
1. 線性異步流程(一步接一步)
最典型的場景是依賴前一步結果的異步操作。比如先登錄獲取 token,再用 token 獲取用户信息,最後用用户信息加載權限配置:
// 用async/await,邏輯一目瞭然
async function initApp(username, password) { // 補充參數定義,避免未定義變量
try {
const token = await login(username, password);
const userInfo = await getUserInfo(token); // 依賴token
const permissions = await getPermissions(userInfo.role); // 依賴userInfo
renderApp(permissions);
} catch (err) {
showError(err);
}
}
如果用 Promise 鏈式調用寫,雖然能實現,但嵌套越深(比如再加兩步),可讀性會明顯下降。
2. 包含條件判斷的異步邏輯
當異步流程中需要根據結果做分支判斷時,async/await 的優勢更明顯。比如:
async function checkAndUpdate() {
const currentVersion = await getCurrentVersion();
// 同步的條件判斷,自然融入異步流程
if (currentVersion < '2.0') {
const updateInfo = await fetchUpdateInfo();
if (updateInfo && updateInfo.force) { // 增加可選鏈,避免updateInfo為undefined時報錯
await showForceUpdateDialog();
} else {
await showOptionalUpdateToast();
}
}
}
這段代碼用 Promise 寫會嵌套多層then,而 async/await 讓同步邏輯和異步操作無縫銜接,就像寫同步代碼一樣自然。
3. 需要中斷執行的場景
有時候我們需要在異步流程中提前返回(比如參數無效時),async/await 的寫法更直觀:
async function submitForm(data) {
// 同步校驗
if (!data?.email) { // 增加可選鏈,避免data為null/undefined時報錯
showError('郵箱不能為空');
return; // 直接中斷
}
// 異步操作
try {
const validateResult = await validateRemote(data);
if (!validateResult.success) {
showError(validateResult.message);
return; // 校驗失敗,中斷
}
await submitData(data);
showSuccess();
} catch (err) {
handleError(err);
}
}
用 Promise 的話,需要在每個then裏處理條件判斷,代碼會更零散。
優先用 Promise 的 3 種場景
雖然 async/await 很方便,但有些場景下,Promise 的原生 API(如Promise.all、Promise.race)更適合,甚至不可替代。
1. 並行執行多個異步操作
當多個異步任務互不依賴時,用Promise.all並行執行能大幅提高效率。比如同時加載列表數據和篩選條件:
async function loadDashboard() {
// 兩個請求並行執行,總耗時是較慢那個的時間
const [products, categories] = await Promise.all([
fetchProducts(),
fetchCategories()
]);
renderProducts(products);
renderFilters(categories);
}
如果用await逐個調用,會變成串行執行(總耗時是兩者之和),完全沒必要:
// 不推薦:串行執行,浪費時間
const products = await fetchProducts();
const categories = await fetchCategories(); // 等第一個完成才開始
2. 需要超時控制的異步操作
Promise.race可以實現 “誰先完成就用誰的結果”,非常適合超時控制。比如 “3 秒內沒返回就顯示加載失敗”:
// 封裝一個帶超時的異步函數
function withTimeout(promise, timeoutMs = 3000) {
let timer; // 將timer提升到外部作用域
const timeoutPromise = new Promise((_, reject) => {
timer = setTimeout(() => {
reject(new Error('請求超時'));
}, timeoutMs);
});
return Promise.race([
promise,
timeoutPromise
]).finally(() => clearTimeout(timer)); // 確保始終清除定時器
}
// 使用
async function loadData() {
try {
const data = await withTimeout(fetchLargeData());
render(data);
} catch (err) {
showError(err.message); // 可能是超時錯誤
}
}
這段代碼用 async/await 無法實現,必須依賴 Promise 的race方法。
3. 處理動態數量的異步任務
當需要處理不確定數量的異步操作(比如批量上傳多個文件),Promise.all是最佳選擇:
async function uploadFiles(files) {
if (!files?.length) return; // 增加空值判斷,避免空數組或undefined時執行無效操作
// 生成一個包含所有上傳Promise的數組
const uploadPromises = files.map(file => {
return uploadFile(file); // 每個文件的上傳是異步操作
});
// 等待所有文件上傳完成
const results = await Promise.all(uploadPromises);
// 處理結果
const successCount = results.filter(r => r?.success).length; // 增加可選鏈容錯
showMessage(`成功上傳 ${successCount}/${files.length} 個文件`);
}
這種動態場景下,Promise 的數組處理能力比 async/await 更高效。
混搭使用:發揮各自優勢
實際開發中,兩者往往結合使用效果最好。比如先並行獲取基礎數據,再串行處理後續邏輯:
async function buildReport() {
// 第一步:並行獲取不相關的數據(提高效率)
const [users, orders, products] = await Promise.all([
fetchUsers(),
fetchOrders(),
fetchProducts()
]);
// 第二步:串行處理依賴關係的邏輯
const userStats = await calculateUserStats(users);
const orderSummary = await generateOrderSummary(orders, userStats); // 依賴userStats
const report = await compileReport(orderSummary, products); // 依賴前兩者
return report;
}
這段代碼先用Promise.all並行請求,節省時間;再用 async/await 處理有依賴的串行邏輯,兼顧效率和可讀性。
避坑指南:這些錯誤別犯
- 不要在循環中直接用 await
循環中用await會導致串行執行,如需並行,改用Promise.all:
// 錯誤:串行執行,慢!
for (const file of files) {
await uploadFile(file);
}
// 正確:並行執行,快!
await Promise.all(files.map(file => uploadFile(file)));
- 別忘了 try/catch
async/await 中任何await的 Promise reject 都會觸發異常,必須用try/catch捕獲,否則會導致程序崩潰。
- 不要把 async 函數當同步函數用
async 函數永遠返回 Promise,調用時必須用await或.then處理,直接調用會拿到 Promise 對象而非結果。
- 避免過度封裝
簡單的異步操作(比如單個請求)沒必要包成 async 函數,直接返回 Promise 更簡潔。
- 注意 Promise.all 的失敗快速失敗特性
Promise.all中任何一個 Promise reject 都會立即觸發整個 Promise reject,如需等待所有結果(無論成功失敗),可使用Promise.allSettled:
// 等待所有任務完成,無論成功失敗
// 文件上傳場景優化
const results = await Promise.allSettled(uploadPromises);
const failedFiles = results
.filter(r => r.status === 'rejected')
.map((r, i) => ({ file: files[i].name, error: r.reason }));
總結:一句話記住用法
- 用 async/await:處理線性依賴、包含條件判斷、需要中斷的異步流程;
- 用 Promise API:處理並行任務(
all)、超時控制(race)、動態異步數組; - 最佳實踐:兩者結合,並行任務用
Promise.all,後續邏輯用 async/await 串聯。
説到底,選擇的核心是可讀性和效率—— 哪種寫法讓團隊成員更容易理解,哪種方式能讓程序跑得更快,就用哪種。技術沒有絕對的好壞,適合場景的才是最好的。