解決async/await頁面卡頓:理解并發處理的正確方法
你可能遇到過這種情況:你在JavaScript中使用了async/await來處理異步操作,比如循環請求用戶列表數據,結果頁面卻長時間白屏,直到所有請求都完成后才顯示內容。這讓你感到困惑:不是說async/await是非阻塞的嗎?它怎么會讓頁面卡住呢?
這個問題觸及了async/await、瀏覽器任務處理和頁面渲染的核心機制。讓我們一步步搞清楚。
誤解澄清:await 到底會不會阻塞?
先說最重要的:async/await本身不會阻塞瀏覽器的JavaScript主線程。 它只是讓寫異步代碼看起來像寫同步代碼的一種方式。
當JavaScript引擎碰到await關鍵字時,它會暫停當前async函數的執行,把控制權交還給瀏覽器的主線程。
這時主線程是空閑的,它可以去做其他事情:響應用戶的點擊、滾動,運行其他腳本代碼,還有最重要的——更新頁面顯示(渲染)。
等到await后面的那個操作(通常是一個Promise)完成后,瀏覽器會在合適的時候(主線程空閑時)把這個async函數暫停的地方繼續執行下去。
聽起來很完美?那為什么頁面還是卡住了呢?
真正的罪魁禍首:一個接一個的等待
問題往往出在代碼怎么寫上。看看下面這個常見的錯誤例子:
// 模擬一個獲取用戶數據的api請求
functionfetchUser(id) {
returnnewPromise(resolve => {
setTimeout(() => {
console.log(`獲取到用戶 ${id}`); // 模擬網絡請求
resolve({ id: id, name: `用戶 ${id}` });
}, 1000); // 假設每個請求需要1秒鐘
});
}
// 錯誤做法:在循環里一個接一個地等
asyncfunctiongetAllUsers(userIds) {
console.time('獲取所有用戶耗時');
const users = [];
for (const id of userIds) {
// 關鍵問題:這里會停下來等,等上一個請求徹底完成,才會開始下一個
const user = await fetchUser(id);
users.push(user);
}
console.timeEnd('獲取所有用戶耗時');
// 假設這里是把用戶數據顯示到頁面上
showUsers(users);
return users;
}
const userIds = [1, 2, 3, 4, 5];
getAllUsers(userIds);
// 控制臺輸出:獲取所有用戶耗時: 約5000毫秒
問題很明顯:這5個請求是一個接一個執行的。
第一個請求發出后,代碼就停下來等它1秒完成,然后才開始第二個請求,再等1秒,如此反復。
總共花了差不多5秒鐘。而更新頁面顯示的那個showUsers(users)函數,必須等到這漫長的5秒全部結束后才會被調用。
在這5秒里,雖然瀏覽器的主線程在每次await等待時確實可以去處理別的事情(比如你點了按鈕它可能還能響應)。
但因為你的代碼邏輯就是讓所有事情排隊做,頁面在等待期間沒有任何新內容可以顯示。
用戶看到的就是一個長時間空白或內容不更新的頁面,感覺就像頁面“卡死”了。
解決之道:讓請求一起 - Promise.all
如果這些請求之間不需要等對方的結果(比如獲取用戶1的數據不需要先知道用戶2的數據),那完全可以讓它們同時發出去!這就是Promise.all的用武之地。
Promise.all接收一個包含多個Promise(代表那些異步操作)的數組。它自己返回一個新的Promise。
這個新Promise會等到數組里所有的Promise都成功完成(resolved)后,才成功,并把所有結果打包成一個數組給你。
改造上面的代碼:
asyncfunctiongetAllUsersFast(userIds) {
console.time('并行獲取所有用戶耗時');
// 1. 創建請求數組:每個元素都是 fetchUser(id) 調用返回的Promise
const userPromises = userIds.map(id => fetchUser(id));
// 2. 使用 Promise.all 等待所有請求完成
const users = awaitPromise.all(userPromises);
console.timeEnd('并行獲取所有用戶耗時'); // 輸出:約1000毫秒
showUsers(users);
return users;
}
getAllUsersFast(userIds);
效果立竿見影!總時間從5秒縮短到了大約1秒(取決于最慢的那個請求)。頁面也能更快地顯示出用戶數據,用戶體驗好得多。
更多實用工具:不同場景用不同方法
Promise.all很強大,但并不是唯一的選擇。根據你的具體需要,還有其他好幫手:
1.Promise.allSettled:每個都要結果,不管成功失敗如果有些請求可能會失敗,但你不想讓一個失敗就中斷所有,還想知道每個請求最終是成功還是失敗了,用Promise.allSettled。
asyncfunctiongetUsersWithStatus(userIds) {
const promises = userIds.map(id => fetchUser(id).catch(error => error)); // 捕獲錯誤,避免整個Promise.allSettled失敗
const results = awaitPromise.allSettled(promises);
// 處理結果:results 是一個數組,每個元素是對象
// { status: 'fulfilled', value: 結果 } 或 { status: 'rejected', reason: 錯誤原因 }
results.forEach(result => {
if (result.status === 'fulfilled') {
console.log('成功:', result.value);
} else {
console.log('失敗:', result.reason);
}
});
return results; // 或者根據 status 過濾出成功的數據
}
2.Promise.race 和 Promise.any:誰快用誰
1)Promise.race: 只要數組里有一個Promise完成(無論是成功還是失敗),它就立刻完成,結果或錯誤就是那個最快的Promise的。
適合做超時控制或者從多個來源取最快響應(比如測哪個CDN快)。
asyncfunctiongetFirstResponse() {
const timeoutPromise = newPromise((_, reject) =>setTimeout(() => reject(newError('超時!')), 500));
const dataPromise = fetchUser(1);
try {
const result = awaitPromise.race([dataPromise, timeoutPromise]);
console.log('成功獲取數據:', result);
} catch (error) {
console.log('出錯或超時:', error.message);
}
}
2)Promise.any: 等待第一個成功完成的Promise。只有數組里所有的Promise都失敗了,它才失敗。適合需要嘗試多個途徑,只要有一個成功就行。
asyncfunctiongetFromAnySource(sources) {
try {
const firstSuccess = awaitPromise.any(sources.map(source => fetch(source)));
console.log('從最快成功的源獲取:', firstSuccess);
} catch (errors) { // 注意:錯誤是 AggregateError
console.log('所有源都失敗了:', errors);
}
}
3)控制同時請求的數量:別把服務器壓垮如果你的用戶ID列表有1000個,用Promise.all會瞬間發出1000個請求。
這可能會讓你的服務器崩潰,或者被瀏覽器限制(瀏覽器通常對同一域名有并發請求數限制,比如6-8個)。
這時候你需要一個“池子”來控制同時進行的請求數量。這里提供一個簡單但有效的實現方法:
asyncfunctionrunWithConcurrency(tasks, maxConcurrent) {
const results = []; // 存放所有任務的最終結果(Promise)
const activeTasks = []; // 當前正在執行的任務對應的Promise(用于跟蹤)
for (const task of tasks) {
// 1. 創建代表當前任務的Promise。`() => task()` 確保任務在需要時才啟動
const taskPromise = Promise.resolve().then(task);
results.push(taskPromise); // 保存結果,最后統一用 Promise.all 等
// 2. 創建任務完成后的清理操作:從 activeTasks 中移除自己
const removeFromActive = () => activeTasks.splice(activeTasks.indexOf(removeFromActive), 1);
activeTasks.push(removeFromActive); // 注意:這里存的是清理函數對應的Promise
// 3. 如果當前活躍任務數已達上限,就等任意一個完成
if (activeTasks.length >= maxConcurrent) {
awaitPromise.race(activeTasks); // 等 activeTasks 數組里任意一個Promise完成
}
// 4. 將清理操作與實際任務完成掛鉤
taskPromise.then(removeFromActive, removeFromActive); // 無論成功失敗都清理
}
// 5. 等待所有任務完成(無論是否在活躍池中)
returnPromise.allSettled(results); // 或者用 Promise.all(results) 只關心成功
}
// 使用示例
const userIds = [1, 2, 3, 4, 5, 6, 7, 8, 9, 10];
// 將 fetchUser(id) 調用包裝成無參數的函數數組
const tasks = userIds.map(id =>() => fetchUser(id));
// 最多同時發出 3 個請求
runWithConcurrency(tasks, 3).then(results => {
console.log('所有用戶獲取完成 (并發控制):', results);
});
這個函數會確保最多只有maxConcurrent個請求同時在進行。
當一個請求完成,池子里有空位了,才會開始下一個請求。在實際項目中,你也可以使用成熟的庫如 p-limit 或 async 的 queue 方法來實現更強大的并發控制。
關鍵總結
- async/await 本身不會阻塞瀏覽器主線程。
- 頁面卡頓通常是因為代碼邏輯(如在循環中串行await)導致了不必要的長時間等待。
- 對于獨立的異步任務(如多個API請求),使用 Promise.all 讓它們并行執行是大幅提升速度和用戶體驗的關鍵。
- 根據需求選擇工具:Promise.allSettled(都要結果)、Promise.race/Promise.any(用最快的)、手動或庫實現的并發控制(防服務器過載)。
- 理解瀏覽器的事件循環和渲染機制有助于寫出更流暢的代碼。記住:長時間的同步邏輯(包括在async函數里連續await造成的等待)會推遲渲染。
掌握這些并發處理技巧,你就能充分利用async/await的優勢,寫出既高效又不會讓用戶感覺頁面卡頓的JavaScript代碼了。