前言 

你可能遇到过这种情况:你在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代码了。

关于优联前端

        武汉优联前端科技有限公司由一批从事前端10余年的专业人才创办,是一家致力于H5前端技术研究的科技创新型公司,为合作伙伴提供专业高效的前端解决方案,合作伙伴遍布中国及东南亚地区,行业涵盖广告,教育, 医疗,餐饮等。有效的解决了合作伙伴的前端技术难题,节约了成本,实现合作共赢。承接开发Web前端,微信小程序、小游戏,2D/3D游戏,动画交互与UI广告设计等各种技术研发。

Logo

有“AI”的1024 = 2048,欢迎大家加入2048 AI社区

更多推荐