解决async/await页面卡顿:理解并发处理的正确方法
摘要: 本文探讨了async/await在JavaScript中的使用误区,指出其本身不会阻塞主线程,但不当的串行写法(如循环内逐个await请求)会导致页面响应延迟。通过对比错误示例与优化方案(如Promise.all并行请求),分析不同场景下的并发控制工具(Promise.allSettled、Promise.race等),并给出控制请求数量的实现方法。核心结论:合理利用并发处理可显著提升异步
前言
你可能遇到过这种情况:你在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很强大,但并不是唯一的选择。根据你的具体需要,还有其他好帮手:
-
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 过滤出成功的数据 }
-
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代码了。
关于优联前端
武汉优联前端科技有限公司由一批从事前端10余年的专业人才创办,是一家致力于H5前端技术研究的科技创新型公司,为合作伙伴提供专业高效的前端解决方案,合作伙伴遍布中国及东南亚地区,行业涵盖广告,教育, 医疗,餐饮等。有效的解决了合作伙伴的前端技术难题,节约了成本,实现合作共赢。承接开发Web前端,微信小程序、小游戏,2D/3D游戏,动画交互与UI广告设计等各种技术研发。
更多推荐
所有评论(0)