解决async/await页面卡顿:理解并发处理的正确方法
你可能遇到过这种情况:你在JavaScript中使用了async/await来处理异步操作,比如循环请求用户列表数据,结果页面却长时间白屏,直到所有请求都完成后才显示内容。这让你感到困惑:不是说async/await是非阻塞的吗?它怎么会让页面卡住呢?
这个问题触及了async/await、浏览器任务处理和页面渲染的核心机制。让我们一步步搞清楚。
误解澄清:await 到底会不会阻塞?
先说最重要的:async/await本身不会阻塞浏览器的JavaScript主线程。 它只是让写异步代码看起来像写同步代码的一种方式。
当JavaScript引擎碰到await关键字时,它会暂停当前async函数的执行,把控制权交还给浏览器的主线程。这时主线程是空闲的,它可以去做其他事情:响应用户的点击、滚动,运行其他脚本代码,还有最重要的——更新页面显示(渲染)。等到await后面的那个操作(通常是一个Promise)完成后,浏览器会在合适的时候(主线程空闲时)把这个async函数暂停的地方继续执行下去。
听起来很完美?那为什么页面还是卡住了呢?
真正的罪魁祸首:一个接一个的等待
问题往往出在代码怎么写上。看看下面这个常见的错误例子:
// 模拟一个获取用户数据的api请求
function fetchUser(id) {
return new Promise(resolve => {
setTimeout(() => {
console.log(`获取到用户 ${id}`); // 模拟网络请求
resolve({ id: id, name: `用户 ${id}` });
}, 1000); // 假设每个请求需要1秒钟
});
}
// 错误做法:在循环里一个接一个地等
async function getAllUsers(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)后,才成功,并把所有结果打包成一个数组给你。
改造上面的代码:
async function getAllUsersFast(userIds) {
console.time('并行获取所有用户耗时');
// 1. 创建请求数组:每个元素都是 fetchUser(id) 调用返回的Promise
const userPromises = userIds.map(id => fetchUser(id));
// 2. 使用 Promise.all 等待所有请求完成
const users = await Promise.all(userPromises);
console.timeEnd('并行获取所有用户耗时'); // 输出:约1000毫秒
showUsers(users);
return users;
}
getAllUsersFast(userIds);效果立竿见影!总时间从5秒缩短到了大约1秒(取决于最慢的那个请求)。页面也能更快地显示出用户数据,用户体验好得多。
更多实用工具:不同场景用不同方法
Promise.all很强大,但并不是唯一的选择。根据你的具体需要,还有其他好帮手:
Promise.allSettled:每个都要结果,不管成功失败
如果有些请求可能会失败,但你不想让一个失败就中断所有,还想知道每个请求最终是成功还是失败了,用Promise.allSettled。async function getUsersWithStatus(userIds) { const promises = userIds.map(id => fetchUser(id).catch(error => error)); // 捕获错误,避免整个Promise.allSettled失败 const results = await Promise.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:谁快用谁
Promise.race: 只要数组里有一个Promise完成(无论是成功还是失败),它就立刻完成,结果或错误就是那个最快的Promise的。适合做超时控制或者从多个来源取最快响应(比如测哪个CDN快)。
async function getFirstResponse() { const timeoutPromise = new Promise((_, reject) => setTimeout(() => reject(new Error('超时!')), 500)); const dataPromise = fetchUser(1); try { const result = await Promise.race([dataPromise, timeoutPromise]); console.log('成功获取数据:', result); } catch (error) { console.log('出错或超时:', error.message); } }Promise.any: 等待第一个成功完成的Promise。只有数组里所有的Promise都失败了,它才失败。适合需要尝试多个途径,只要有一个成功就行。
async function getFromAnySource(sources) { try { const firstSuccess = await Promise.any(sources.map(source => fetch(source))); console.log('从最快成功的源获取:', firstSuccess); } catch (errors) { // 注意:错误是 AggregateError console.log('所有源都失败了:', errors); } }
控制同时请求的数量:别把服务器压垮
如果你的用户ID列表有1000个,用Promise.all会瞬间发出1000个请求。这可能会让你的服务器崩溃,或者被浏览器限制(浏览器通常对同一域名有并发请求数限制,比如6-8个)。这时候你需要一个“池子”来控制同时进行的请求数量。这里提供一个简单但有效的实现方法:
async function runWithConcurrency(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) { await Promise.race(activeTasks); // 等 activeTasks 数组里任意一个Promise完成 } // 4. 将清理操作与实际任务完成挂钩 taskPromise.then(removeFromActive, removeFromActive); // 无论成功失败都清理 } // 5. 等待所有任务完成(无论是否在活跃池中) return Promise.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代码了。
本文内容仅供个人学习/研究/参考使用,不构成任何决策建议或专业指导。分享/转载时请标明原文来源,同时请勿将内容用于商业售卖、虚假宣传等非学习用途哦~感谢您的理解与支持!