醋醋百科网

Good Luck To You!

为何 async/await 会“阻塞”页面?并发处理的正确姿势

async/await 让我们能用同步的方式书写异步代码,告别了恼人的“回调地狱”。然而,一个经典的场景常常让开发者感到困惑:

场景: 我需要循环请求一个用户列表,为什么用了 async/await 之后,页面会长时间白屏,直到所有请求都完成后才显示内容?async/await 不是非阻塞的吗?它怎么会阻塞页面渲染呢?

这一个问题触及了 async/await、事件循环(Event Loop)和浏览器渲染机制的核心。

误解澄清:await阻塞的是什么?

首先,我们必须明确一个核心概念:

async/await 本身绝不会阻塞 JavaScript 主线程,它是一种非阻塞的语法糖

当 JavaScript 引擎遇到 await 关键字时,它会暂停当前 async 函数的执行,将控制权交还给主线程。主线程此时是自由的,可以去处理其他任务,比如响应用户输入、执行其他脚本、以及最重要的——进行页面渲染。当 await 后面的 Promise 完成后,事件循环会再将 async 函数的后续代码推入任务队列,等待主线程空闲时恢复执行。

听起来很完美,那为什么我们的页面还是被“阻塞”了呢?

真正的元凶:串行执行的 await

让我们来看看那个导致“阻塞感”的罪魁祸首代码:

// 模拟一个 API 请求
function fetchUser(id) {
 return new Promise(resolve => {
    setTimeout(() => {
      console.log(`Fetched user ${id}`);
      resolve({ id: id, name: `User ${id}` });
    }, 1000); // 每个请求耗时 1 秒
  });
}

// 错误示范:在 for 循环中串行使用 await
async function fetchAllUsers(userIds) {
 console.time('Fetch All Users');
 const users = [];
 for (const id of userIds) {
    // 关键点:循环会在这里暂停,等待上一个请求完成后再开始下一个
    const user = await fetchUser(id);
    users.push(user);
  }
 console.timeEnd('Fetch All Users');
 // 假设这里是更新 UI 的操作
 renderUsers(users); 
 return users;
}

const userIds = [1, 2, 3, 4, 5];
fetchAllUsers(userIds); 
// 控制台输出:Fetch All Users: 5005.12ms

问题显而易见: 这 5 个请求是串行的,一个接一个地执行。总耗时约等于所有请求耗时之和(5秒)。renderUsers(users) 这个最终更新 UI 的操作,必须等到这漫长的 5 秒全部结束后才能被调用。

在这 5 秒钟内,虽然主线程没有被 await 本身阻塞(它在 await 期间可以响应别的事件),但我们的业务逻辑人为地创造了一个漫长的等待。用户看到的就是一个长时间不更新的页面,这就是“阻塞感”的来源。

并发处理的正确姿势:Promise.all

那么,如何将这些串行的请求变成并行的呢?这些请求之间并没有依赖关系,完全可以同时发出!答案就是 Promise.all

Promise.all 接收一个 Promise 数组作为参数,它会返回一个新的 Promise。这个新的 Promise 会在所有输入的 Promise 都成功(fulfilled)后才成功,并将所有结果汇总成一个数组返回。

让我们来改造一下上面的代码:

// 正确示范:使用 Promise.all 并发处理
async function fetchAllUsersConcurrently(userIds) {
    console.time('Concurrent Fetch');

    // 1. 将所有请求(Promise)放入一个数组,此时请求已经全部发出!
    const promises = userIds.map(id => fetchUser(id));

    // 2. 使用 Promise.all 等待所有请求完成
    const users = await Promise.all(promises);
    console.timeEnd('Concurrent Fetch');
    renderUsers(users);
    return users;
}

fetchAllUsersConcurrently(userIds);
// 控制台输出: Concurrent Fetch: 1008.25ms

总耗时从 5 秒骤降至 1 秒!这才是我们想要的效率,UI 也能更快地得到更新。

进阶:更多并发控制工具

Promise.all 非常强大,但它不是唯一的工具。在不同场景下,我们还有更合适的选择。

1. Promise.allSettled:不在乎失败,只在乎结果

Promise.all 有个“缺点”:只要有一个 Promise 失败(rejected),它就会立即失败,并且不会返回任何已成功的结果。如果我们希望无论成功与否,都等待所有请求完成,并获取它们各自的状态,Promise.allSettled 是我们的不二之选。

// fetchUser(3) 会失败
// const promises = [fetchUser(1), fetchUser(2), fetchUserThatFails(3)];
// const results = await Promise.allSettled(promises);

/* results 会是这样:
[
  { 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 会以第一个 settle(无论是成功还是失败)的 Promise 的结果为准。适用于需要从多个源获取数据,但只用最快返回的那个的场景(比如CDN测速)。
  • Promise.any:返回的 Promise 会以第一个成功(fulfilled)的 Promise 的结果为准。如果所有 Promise 都失败了,它才会失败。

3. 控制并发数量:避免瞬间打垮服务器

如果 userIds 的长度是 1000 呢?使用 Promise.all 会瞬间发出 1000 个请求,这可能会对我们的服务器造成巨大压力,甚至触发浏览器的并发请求数限制。

这时,我们需要一个“并发池”来控制同时进行的任务数量。我们可以手动实现一个简单的并发控制器:

async function limitedConcurrency(tasks, limit) {
 const results = [];
 const executing = []; // 正在执行的任务

 for (const task of tasks) {
    // 1. 创建并开始一个任务的 Promise
    const p = Promise.resolve().then(() => task());
    results.push(p); // 存储 Promise 的最终结果

    // 2. 当任务执行完毕后,从 executing 数组中移除
    if (limit <= tasks.length) {
      const e = p.then(() => executing.splice(executing.indexOf(e), 1));
      executing.push(e);
      
      // 3. 如果正在执行的任务达到上限,就等待其中一个完成
      if (executing.length >= limit) {
        await Promise.race(executing);
      }
    }
  }

 return Promise.all(results);
}

// 使用方法
const userIds = [1, 2, 3, 4, 5, 6, 7];
// 将 fetchUser 调用包装成无参函数
const tasks = userIds.map(id => () => fetchUser(id));

// 同时只允许 3 个请求并发
limitedConcurrency(tasks, 3).then(users => {
 console.log('All users fetched with limited concurrency:', users);
});

这个函数会确保同时在“飞行”的请求数量不会超过 limit。当然,在实际项目中,我们也可以使用成熟的第三方库来更优雅地解决这个问题。

控制面板
您好,欢迎到访网站!
  查看权限
网站分类
最新留言