为何 async/await 会“阻塞”页面?并发处理的正确姿势
async/await
是现代 JavaScript 的利器,它让我们用同步的方式书写异步代码,告别回调地狱。然而,在实际开发中,很多人会遇到一个困惑场景:
我需要循环请求一个用户列表,使用 async/await 后,页面长时间白屏,直到所有请求完成才显示内容。async/await 不是非阻塞的吗?怎么会阻塞页面渲染?
本文将带你深入理解其本质,并教你正确的并发处理方法。
误区澄清:await 阻塞的不是主线程
核心概念:
async/await
本身不会阻塞主线程,它是非阻塞的语法糖执行机制:
- 遇到
await
,当前 async 函数暂停执行 - 控制权交回主线程,主线程可处理其他任务(UI 渲染、事件响应等)
- Promise 完成后,async 函数的后续代码被放入任务队列,等待主线程空闲时继续执行
- 遇到
看似完美,但问题仍然存在。
真正的“阻塞感”元凶:串行执行的 await
async function fetchAllUsers(userIds) {
const users = [];
for (const id of userIds) {
const user = await fetchUser(id); // 串行等待
users.push(user);
}
renderUsers(users);
}
问题:
- 5 个请求依次串行,总耗时≈5秒
renderUsers
必须等待所有请求完成- 虽然主线程未被 await 阻塞,但用户看到的是长时间未更新的页面
- 用户体验上等同“阻塞”
并发处理的正确姿势:Promise.all
当请求之间没有依赖时,可以同时发起多个请求:
const promises = userIds.map(id => fetchUser(id));
const users = await Promise.all(promises);
renderUsers(users);
效果:
- 总耗时从 5 秒降至约 1 秒
- UI 更快更新
- 用户体验大幅提升
进阶:更多并发控制工具
1. Promise.allSettled
:获取全部结果
const results = await Promise.allSettled([fetchUser(1), fetchUser(2), fetchUserThatFails()]);
- 不在乎失败,只在乎每个 Promise 的最终状态
- 返回格式示例:
[
{ 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.any:第一个成功(fulfilled)的结果
适合 CDN测速或优先使用最快数据源场景。
3. 控制并发数量:避免瞬间打垮服务器
当任务量巨大时(如 1000 个请求),直接 Promise.all
会瞬间发出大量请求,可能触发浏览器限制或服务器压力。
示例:并发池控制任务数量
async function limitedConcurrency(tasks, limit) {
const results = [];
const executing = [];
for (const task of tasks) {
const p = Promise.resolve().then(() => task());
results.push(p);
if (limit <= tasks.length) {
const e = p.then(() => executing.splice(executing.indexOf(e), 1));
executing.push(e);
if (executing.length >= limit) {
await Promise.race(executing);
}
}
}
return Promise.all(results);
}
const tasks = userIds.map(id => () => fetchUser(id));
limitedConcurrency(tasks, 3).then(users => {
console.log('All users fetched with limited concurrency:', users);
});
- 确保同时执行的任务数不超过 limit
- 灵活、可控,避免瞬间打垮服务器
总结
- async/await 不阻塞主线程,阻塞感来源于串行等待
- 串行 await → 总耗时累加,UI 延迟更新
- 并发执行(
Promise.all
、Promise.allSettled
等) → 高效、快速更新 UI - 并发池 → 控制大量请求的并发数量,保护服务器
正确理解 async/await 与并发,才能写出既安全又高性能的前端代码。