為何 async/await 會“阻塞”頁面?并發處理的正確姿勢
async/await 讓我們能用同步的方式書寫異步代碼,告別了惱人的“回調地獄”。然而,一個經典的場景常常讓開發者感到困惑:
場景: 我需要循環請求一個用戶列表,為什么用了 async/await 之后,頁面會長時間白屏,直到所有請求都完成后才顯示內容?async/await 不是非阻塞的嗎?它怎么會阻塞頁面渲染呢?
這一個問題觸及了 async/await、事件循環(Event Loop)和瀏覽器渲染機制的核心。

一、誤解澄清:await 阻塞的是什么?
首先,我們必須明確一個核心概念:
async/await 本身絕不會阻塞 JavaScript 主線程,它是一種非阻塞的語法糖
當 JavaScript 引擎遇到 await 關鍵字時,它會暫停當前 async 函數的執行,將控制權交還給主線程。主線程此時是自由的,可以去處理其他任務,比如響應用戶輸入、執行其他腳本、以及最重要的——進行頁面渲染。當 await 后面的 Promise 完成后,事件循環會再將 async 函數的后續代碼推入任務隊列,等待主線程空閑時恢復執行。
聽起來很完美,那為什么我們的頁面還是被“阻塞”了呢?
二、真正的元兇:串行執行的 await
讓我們來看看那個導致“阻塞感”的罪魁禍首代碼:
// 模擬一個 API 請求
function fetchUser(id) {
return new Promise(resolve => {
setTimeout(() => {
console.log(`Fetched user ${id}`);
resolve({ id: id, name: `User ${id}` });
}, 1000); // 每個請求耗時 1 秒
});
}
// 錯誤示范:在 for 循環中串行使用 await
async function fetchAllUsers(userIds) {
console.time('Fetch All Users');
const users = [];
for (const id of userIds) {
// 關鍵點:循環會在這里暫停,等待上一個請求完成后再開始下一個
const user = await fetchUser(id);
users.push(user);
}
console.timeEnd('Fetch All Users');
// 假設這里是更新 UI 的操作
renderUsers(users);
return users;
}
const userIds = [1, 2, 3, 4, 5];
fetchAllUsers(userIds);
// 控制臺輸出:Fetch All Users: 5005.12ms問題顯而易見: 這 5 個請求是串行的,一個接一個地執行。總耗時約等于所有請求耗時之和(5秒)。renderUsers(users) 這個最終更新 UI 的操作,必須等到這漫長的 5 秒全部結束后才能被調用。
在這 5 秒鐘內,雖然主線程沒有被 await 本身阻塞(它在 await 期間可以響應別的事件),但我們的業務邏輯人為地創造了一個漫長的等待。用戶看到的就是一個長時間不更新的頁面,這就是“阻塞感”的來源。
三、并發處理的正確姿勢:Promise.all
那么,如何將這些串行的請求變成并行的呢?這些請求之間并沒有依賴關系,完全可以同時發出!答案就是 Promise.all。
Promise.all 接收一個 Promise 數組作為參數,它會返回一個新的 Promise。這個新的 Promise 會在所有輸入的 Promise 都成功(fulfilled)后才成功,并將所有結果匯總成一個數組返回。
讓我們來改造一下上面的代碼:

總耗時從 5 秒驟降至 1 秒!這才是我們想要的效率,UI 也能更快地得到更新。
四、進階:更多并發控制工具
Promise.all 非常強大,但它不是唯一的工具。在不同場景下,我們還有更合適的選擇。
1. Promise.allSettled:不在乎失敗,只在乎結果
Promise.all 有個“缺點”:只要有一個 Promise 失敗(rejected),它就會立即失敗,并且不會返回任何已成功的結果。如果我們希望無論成功與否,都等待所有請求完成,并獲取它們各自的狀態,Promise.allSettled 是我們的不二之選。
// fetchUser(3) 會失敗
// const promises = [fetchUser(1), fetchUser(2), fetchUserThatFails(3)];
// const results = await Promise.allSettled(promises);
/* results 會是這樣:
[
{ status: 'fulfilled', value: { id: 1, ... } },
{ status: 'fulfilled', value: { id: 2, ... } },
{ status: 'rejected', reason: 'Error: User not found' }
]
*/2. Promise.race & Promise.any:誰快用誰
Promise.race:賽跑。返回的 Promise 會以第一個 settle(無論是成功還是失敗)的 Promise 的結果為準。適用于需要從多個源獲取數據,但只用最快返回的那個的場景(比如CDN測速)。
Promise.any:返回的 Promise 會以第一個成功(fulfilled)的 Promise 的結果為準。如果所有 Promise 都失敗了,它才會失敗。
3. 控制并發數量:避免瞬間打垮服務器
如果 userIds 的長度是 1000 呢?使用 Promise.all 會瞬間發出 1000 個請求,這可能會對我們的服務器造成巨大壓力,甚至觸發瀏覽器的并發請求數限制。
這時,我們需要一個“并發池”來控制同時進行的任務數量。我們可以手動實現一個簡單的并發控制器:
async function limitedConcurrency(tasks, limit) {
const results = [];
const executing = []; // 正在執行的任務
for (const task of tasks) {
// 1. 創建并開始一個任務的 Promise
const p = Promise.resolve().then(() => task());
results.push(p); // 存儲 Promise 的最終結果
// 2. 當任務執行完畢后,從 executing 數組中移除
if (limit <= tasks.length) {
const e = p.then(() => executing.splice(executing.indexOf(e), 1));
executing.push(e);
// 3. 如果正在執行的任務達到上限,就等待其中一個完成
if (executing.length >= limit) {
await Promise.race(executing);
}
}
}
return Promise.all(results);
}
// 使用方法
const userIds = [1, 2, 3, 4, 5, 6, 7];
// 將 fetchUser 調用包裝成無參函數
const tasks = userIds.map(id => () => fetchUser(id));
// 同時只允許 3 個請求并發
limitedConcurrency(tasks, 3).then(users => {
console.log('All users fetched with limited concurrency:', users);
});這個函數會確保同時在“飛行”的請求數量不會超過 limit。當然,在實際項目中,我們也可以使用成熟的第三方庫來更優雅地解決這個問題。































