JS `Promise.all()`:并发执行多个 Promise 并等待全部完成

各位观众老爷,晚上好!我是你们的老朋友,Bug终结者,今天咱们来聊聊JavaScript里一个非常实用,但有时候也容易让人掉坑里的API:Promise.all()

别看名字挺唬人,其实它干的事情很简单,就是并发执行一堆Promise,然后等你指定的这些Promise 全部 都完成了,它才会给你一个最终的结果。就好像你同时烤好几个披萨,只有所有披萨都烤好了,你才能开开心心地享用它们。

Promise.all()的基本用法

Promise.all()接受一个可迭代对象(通常是数组),里面包含了一堆Promise。它会返回一个新的Promise,这个新的Promise的状态取决于参数中所有Promise的状态:

  • 成功: 如果所有Promise都成功了,那么返回的Promise也会成功,并且它的resolve值是一个数组,包含所有Promise的resolve值,顺序和传入的Promise顺序一致。
  • 失败: 只要有一个Promise失败了,那么返回的Promise就会立即失败,并且它的reject值是第一个失败的Promise的reject值。

来看个简单的例子:

const promise1 = Promise.resolve(3);
const promise2 = 42; // 注意:这里不是Promise,会被自动包装成Promise.resolve(42)
const promise3 = new Promise((resolve, reject) => {
  setTimeout(resolve, 100, 'foo'); // 100ms后resolve
});

Promise.all([promise1, promise2, promise3])
  .then((values) => {
    console.log(values); // 输出: [3, 42, 'foo'],大约100ms后输出
  })
  .catch((error) => {
    console.error(error); // 不会执行到这里,因为所有promise都成功了
  });

在这个例子中,promise1promise2已经处于resolved状态,promise3会在100ms后resolve。所以,Promise.all()会在大约100ms后resolve,并将所有Promise的resolve值组成一个数组返回。

Promise.all()的错误处理

重点来了!如果其中一个Promise失败了,Promise.all()会立即reject,并且只返回第一个失败的Promise的reject值。这意味着,其他已经resolve的Promise的结果会被忽略,没有被处理。

const promise1 = Promise.resolve(3);
const promise2 = Promise.reject('出错了!'); // 立即reject
const promise3 = new Promise((resolve, reject) => {
  setTimeout(resolve, 100, 'foo'); // 100ms后resolve,但没用了
});

Promise.all([promise1, promise2, promise3])
  .then((values) => {
    console.log(values); // 不会执行到这里
  })
  .catch((error) => {
    console.error(error); // 输出: 出错了!,立即输出
  });

在这个例子中,promise2立即reject,所以Promise.all()也会立即reject,并返回promise2的reject值。即使promise1已经resolve,promise3最终也会resolve,但这些结果都会被忽略。

应用场景:并发请求数据

Promise.all()最常见的应用场景就是并发请求多个数据接口,然后等待所有数据都返回后再进行处理。例如,你需要从三个不同的API获取用户信息、用户订单和用户地址,你可以这样做:

function getUserInfo(userId) {
  return fetch(`/api/user/${userId}`).then(response => response.json());
}

function getUserOrders(userId) {
  return fetch(`/api/orders/${userId}`).then(response => response.json());
}

function getUserAddress(userId) {
  return fetch(`/api/address/${userId}`).then(response => response.json());
}

async function displayUserData(userId) {
  try {
    const [userInfo, userOrders, userAddress] = await Promise.all([
      getUserInfo(userId),
      getUserOrders(userId),
      getUserAddress(userId)
    ]);

    console.log('用户信息:', userInfo);
    console.log('用户订单:', userOrders);
    console.log('用户地址:', userAddress);

    // 在这里进行数据处理和展示
  } catch (error) {
    console.error('获取数据失败:', error);
    // 处理错误,例如显示错误信息
  }
}

displayUserData(123);

这个例子中,Promise.all()并发执行三个fetch请求,只有当所有请求都成功返回数据后,才会将数据传递给then回调函数。如果其中任何一个请求失败,catch回调函数会被调用,你可以进行错误处理。

Promise.all()的局限性

虽然Promise.all()很强大,但它也有一些局限性:

  • 短板效应: 只要有一个Promise失败,整个Promise.all()就会失败,这可能会导致一些已经完成的Promise的结果被浪费。
  • 没有取消功能: 无法取消正在执行的Promise。如果某个Promise永远无法resolve或reject,Promise.all()就会一直等待下去。
  • 错误处理: 只能捕获第一个失败的Promise的错误,无法获取所有失败的Promise的错误信息。

替代方案:Promise.allSettled()

为了解决Promise.all()的短板效应,ES2020引入了Promise.allSettled()。它会等待所有Promise都settled(即resolve或reject),然后返回一个数组,包含每个Promise的结果。

const promise1 = Promise.resolve(3);
const promise2 = Promise.reject('出错了!');
const promise3 = new Promise((resolve, reject) => {
  setTimeout(resolve, 100, 'foo');
});

Promise.allSettled([promise1, promise2, promise3])
  .then((results) => {
    console.log(results);
    /*
      输出:
      [
        { status: 'fulfilled', value: 3 },
        { status: 'rejected', reason: '出错了!' },
        { status: 'fulfilled', value: 'foo' }
      ]
    */
  });

Promise.allSettled()返回的数组中,每个元素都是一个对象,包含以下属性:

  • status: 'fulfilled''rejected',表示Promise的状态。
  • value: 如果Promise是fulfilled状态,则包含resolve值。
  • reason: 如果Promise是rejected状态,则包含reject值。

使用Promise.allSettled()可以避免短板效应,你可以处理所有Promise的结果,即使其中一些Promise失败了。

Promise.race()

还有一个和Promise.all()类似,但行为相反的API:Promise.race()。 它接受一个可迭代对象,里面包含了一堆Promise。它会返回一个新的Promise,这个新的Promise的状态取决于参数中第一个改变状态的Promise:

  • 成功: 如果第一个resolve的Promise成功了,那么返回的Promise也会成功,并且它的resolve值是第一个resolve的Promise的resolve值。
  • 失败: 如果第一个reject的Promise失败了,那么返回的Promise也会立即失败,并且它的reject值是第一个reject的Promise的reject值。

就好像赛跑一样,谁先到达终点,就以谁的结果为准。

const promise1 = new Promise((resolve, reject) => {
  setTimeout(resolve, 500, 'one');
});

const promise2 = new Promise((resolve, reject) => {
  setTimeout(reject, 100, 'two');
});

Promise.race([promise1, promise2])
  .then((value) => {
    console.log(value); // 不会执行到这里
  })
  .catch((error) => {
    console.log(error); // 输出: two,大约100ms后输出
  });

在这个例子中,promise2会在100ms后reject,而promise1会在500ms后resolve。因为promise2先改变了状态,所以Promise.race()会立即reject,并返回promise2的reject值。

应用场景:超时控制

Promise.race()的一个常见应用场景是超时控制。你可以创建一个Promise,在指定的时间后reject,然后将它和你的实际Promise一起传递给Promise.race()。如果实际的Promise在超时时间内没有resolve或reject,那么超时Promise会reject,从而导致Promise.race() reject。

function timeout(ms) {
  return new Promise((resolve, reject) => {
    setTimeout(() => {
      reject('超时了!');
    }, ms);
  });
}

function fetchData() {
  return new Promise((resolve, reject) => {
    // 模拟一个耗时操作
    setTimeout(() => {
      resolve('数据获取成功!');
    }, 2000);
  });
}

async function getDataWithTimeout() {
  try {
    const data = await Promise.race([fetchData(), timeout(1000)]);
    console.log(data); // 如果 fetchData 在 1000ms 内 resolve,则输出数据
  } catch (error) {
    console.error(error); // 如果 fetchData 超过 1000ms,则输出 "超时了!"
  }
}

getDataWithTimeout();

在这个例子中,timeout(1000)会创建一个Promise,在1000ms后reject。Promise.race()会将这个Promise和fetchData()返回的Promise一起执行。如果fetchData()在1000ms内resolve,那么Promise.race()会resolve,并返回fetchData()的结果。如果fetchData()超过1000ms还没有resolve,那么timeout(1000)会reject,导致Promise.race() reject。

总结

API 作用 成功条件 失败条件 适用场景
Promise.all() 并发执行多个Promise,等待所有Promise都完成。 所有Promise都resolve。 任意一个Promise reject。 并行执行独立的异步操作,需要所有操作都成功才能进行下一步。
Promise.allSettled() 并发执行多个Promise,等待所有Promise都settled(resolve或reject)。 所有Promise都settled。 无,总是会resolve。 并行执行独立的异步操作,无论操作成功与否都需要进行下一步(例如,记录日志)。
Promise.race() 并发执行多个Promise,等待第一个Promise改变状态。 第一个Promise resolve。 第一个Promise reject。 竞速场景,例如超时控制,或者选择最快的响应。

希望通过今天的讲解,大家能够更深入地了解Promise.all()Promise.allSettled()Promise.race()的用法和注意事项。 记住,合理使用这些API可以提高代码的效率和可维护性,但也要注意它们各自的局限性,选择最适合你的场景的API。

今天的分享就到这里,感谢各位的收看!我们下次再见!

发表回复

您的邮箱地址不会被公开。 必填项已用 * 标注