解决async/await页面卡顿:理解并发处理的正确方法

更新日期: 2025-07-19阅读: 68标签: 异步

你可能遇到过这种情况:你在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很强大,但并不是唯一的选择。根据你的具体需要,还有其他好帮手:

  1. 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 过滤出成功的数据
    }
  2. 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);
        }
      }
  3. 控制同时请求的数量:别把服务器压垮
    如果你的用户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代码了。

本文内容仅供个人学习/研究/参考使用,不构成任何决策建议或专业指导。分享/转载时请标明原文来源,同时请勿将内容用于商业售卖、虚假宣传等非学习用途哦~感谢您的理解与支持!

链接: https://fly63.com/article/detial/12879

通过alert方法,去理解js中阻塞、局部作用域、同步/异步任务

javascript中alert是Bom中的成员函数,alert对话框是模态的,具有阻塞性质的,不点击是不会执行后续代码的。js的阻塞是指在调用结果返回之前,当前线程会被挂起, 只有在得到结果之后才会继续执行。

如何优化async代码?更好的编写async异步函数

如何优化async代码?更好的编写async函数:使用return Promise.reject()在async函数中抛出异常,让相互之间没有依赖关系的异步函数同时执行,不要在循环的回调中/for、while循环中使用await,用map来代替它

【JS】异步处理机制的几种方式

Javascript语言的执行环境是单线程,异步模式非常重要。在浏览器端,耗时很长的操作都应该异步执行,避免浏览器失去响应,最好的例子就是Ajax操作。

js异步加载方式有哪些?_详解异步加载js的多种方案

js异步加载又被称为非阻塞加载,浏览器在下载JS的同时,还会进行后续页面处理。那么如何实现js异步加载呢?下面整理了多种实现方案供大家参考。异步加载js方案:Script Dom Element、onload时的异步加载、$(document).ready()、async属性、defer属性、es6模块type=module属性

Nodejs 处理异步(获取异步数据并处理)的方法

回调函数方式:将异步方法如readFile封装到一个自定义函数中,通过将异步方法得到的结果传给自定义方法的回调函数参数。事件驱动方式:使用node events模块,利用其EventEmitter对象

JS常用的几种异步流程控制

JavaScript引擎是基于单线程 (Single-threaded) 事件循环的概念构建的,同一时刻只允许一个代码块在执行,所以需要跟踪即将运行的代码,那些代码被放在一个任务队列 (job queue) 中

前端异步编程之Promise和async的用法

传统的异步解决方案采用回调函数和事件监听的方式,而这里主要记录两种异步编程的新方案:ES6的新语法Promise;ES2017引入的async函数;Generator函数(略)

异步的JavaScript

JS本身是一门单线程的语言,所以在执行一些需要等待的任务(eg.等待服务器响应,等待用户输入等)时就会阻塞其他代码。如果在浏览器中JS线程阻塞了,浏览器可能会失去响应,从而造成不好的用户体验。

js 多个异步的并发控制

请实现如下的函数,可以批量请求数据,所有的URL地址在urls参数中,同时可以通过max参数 控制请求的并发度。当所有的请求结束后,需要执行callback回调。发请求的函数可以直接使用fetch。

解读react的setSate的异步问题

将setState()认为是一次请求而不是一次立即执行更新组件的命令。为了更为可观的性能,React可能会推迟它,稍后会一次性更新这些组件。React不会保证在setState之后,能够立刻拿到改变的结果。

点击更多...

内容以共享、参考、研究为目的,不存在任何商业目的。其版权属原作者所有,如有侵权或违规,请与小编联系!情况属实本人将予以删除!