JS `Promise.allSettled` 与 `Promise.any` (ES2021):并发控制与结果处理

各位观众老爷,大家好!今天咱们来聊聊 JavaScript 里两位并发控制的“狠角色”:Promise.allSettledPromise.any (ES2021)。这俩哥们儿都是用来处理多个 Promise 的,但脾气秉性可不太一样。咱们这就来扒一扒他们的底细,看看在实际开发中怎么用好他们。

开场白:Promise 的并发困境

在异步编程的世界里,Promise 就像我们的快递小哥,辛辛苦苦地把结果送到我们手上。但如果我们要同时寄很多快递(发起多个 Promise),就得想办法有效地管理这些小哥。

传统的 Promise.all 就像一个严苛的监工,只要有一个快递小哥出了岔子(Promise rejected),整个任务就宣告失败,直接罢工。这在某些场景下显得过于死板,不够人性化。

Promise.race 则像一场赛跑,谁先送到就算谁赢,其他小哥直接被淘汰,这在需要所有结果的场景下就不适用了。

所以,我们需要更灵活的并发控制手段,这就是 Promise.allSettledPromise.any 登场的原因。

第一位选手:Promise.allSettled – 稳健先生

Promise.allSettled 就像一个老好人,无论 Promise 是 fulfilled 还是 rejected,他都会老老实实地把结果收集起来,一个都不落下。

语法:

Promise.allSettled([promise1, promise2, promise3, ...])
  .then(results => {
    // 处理所有 Promise 的结果
  });

特性:

  • 永远不会 rejected: 无论传入的 Promise 最终是 fulfilled 还是 rejected,Promise.allSettled 自身都会 fulfilled,并且返回一个包含所有 Promise 结果的数组。
  • 返回结果格式: 返回的数组中,每个元素都是一个对象,包含 statusvalue (fulfilled 时) 或 reason (rejected 时) 属性。

代码示例:

const promise1 = Promise.resolve(1);
const promise2 = Promise.reject('Error 2');
const promise3 = new Promise(resolve => setTimeout(() => resolve(3), 100));

Promise.allSettled([promise1, promise2, promise3])
  .then(results => {
    console.log(results);
    /*
    [
      { status: 'fulfilled', value: 1 },
      { status: 'rejected', reason: 'Error 2' },
      { status: 'fulfilled', value: 3 }
    ]
    */

    // 区分 fulfilled 和 rejected,进行不同的处理
    results.forEach(result => {
      if (result.status === 'fulfilled') {
        console.log('Fulfilled:', result.value);
      } else {
        console.error('Rejected:', result.reason);
      }
    });
  });

应用场景:

  • 监控多个异步操作的状态: 当需要知道所有异步操作的成功与失败情况时,Promise.allSettled 非常有用。例如,同时上传多个文件,无论成功与否,都需要通知用户上传结果。
  • 容错处理: 在某些场景下,即使某些 Promise rejected,也希望继续执行后续操作,而不是直接中断。例如,从多个 API 获取数据,即使某些 API 请求失败,也希望展示其他 API 返回的数据。
  • 并行执行,记录结果: 并行执行多个任务,并记录每个任务的执行结果,以便进行统计分析或错误排查。

表格总结:

特性 说明
返回值 一个 Promise,fulfilled 后返回一个数组,数组中每个元素都是一个对象,包含 status (fulfilled 或 rejected) 和 value (fulfilled 时) 或 reason (rejected 时) 属性。
错误处理 永远不会 rejected,所有错误信息都会包含在返回结果中。
适用场景 需要知道所有 Promise 的成功与失败状态,并进行相应的处理。
优点 容错性强,不会因为某个 Promise rejected 而中断整个流程。
缺点 需要手动区分 fulfilled 和 rejected,代码相对繁琐。

第二位选手:Promise.any – 乐观主义者

Promise.any 就像一个乐观主义者,只要有一个 Promise fulfilled,他就认为整个任务成功了,立刻返回第一个 fulfilled 的 Promise 的值。如果所有 Promise 都 rejected,他才会抛出一个 AggregateError 错误。

语法:

Promise.any([promise1, promise2, promise3, ...])
  .then(value => {
    // 处理第一个 fulfilled 的 Promise 的值
  })
  .catch(error => {
    // 处理所有 Promise 都 rejected 的情况
  });

特性:

  • 返回第一个 fulfilled 的 Promise 的值: 只要有一个 Promise fulfilled,Promise.any 就会立即 fulfilled,并返回该 Promise 的值。
  • 如果所有 Promise 都 rejected,则 rejected: 如果所有 Promise 都 rejected,Promise.any 就会 rejected,并抛出一个 AggregateError 错误,其中包含所有 rejected 的原因。

代码示例:

const promise1 = Promise.reject('Error 1');
const promise2 = Promise.resolve(2);
const promise3 = new Promise((resolve, reject) => setTimeout(() => reject('Error 3'), 100));

Promise.any([promise1, promise2, promise3])
  .then(value => {
    console.log('Fulfilled:', value); // 输出:Fulfilled: 2
  })
  .catch(error => {
    console.error('All promises rejected:', error.errors);
    /*
    All promises rejected: [ 'Error 1', 'Error 3' ]
    */
  });

const promise4 = Promise.reject('Error 4');
const promise5 = Promise.reject('Error 5');

Promise.any([promise4, promise5])
  .then(value => {
    console.log('Fulfilled:', value);
  })
  .catch(error => {
    console.error('All promises rejected:', error.errors);
    /*
    All promises rejected: [ 'Error 4', 'Error 5' ]
    */
  });

应用场景:

  • 备选方案: 当有多个备选方案时,只要有一个方案成功,就可以认为整个任务成功。例如,从多个 CDN 加载资源,只要有一个 CDN 加载成功,就可以使用该资源。
  • 服务降级: 当主服务不可用时,可以使用备用服务。Promise.any 可以用来尝试使用多个服务,只要有一个服务可用,就可以继续提供服务。
  • 延迟加载: 当需要加载多个资源时,可以并行加载这些资源,只要有一个资源加载完成,就可以先展示该资源,提高用户体验。

表格总结:

特性 说明
返回值 一个 Promise,如果至少有一个 Promise fulfilled,则返回第一个 fulfilled 的 Promise 的值。如果所有 Promise 都 rejected,则 rejected,并抛出一个 AggregateError 错误,其中包含所有 rejected 的原因。
错误处理 如果所有 Promise 都 rejected,则 rejected,并抛出一个 AggregateError 错误。
适用场景 只需要一个 Promise fulfilled 即可,例如备选方案、服务降级等。
优点 可以快速返回结果,提高效率。
缺点 如果所有 Promise 都 rejected,则会抛出错误,需要进行错误处理。并且只有在所有 promise 都reject时才会抛出错误,如果部分reject,部分pending,则会一直等待直到有promise resolve或reject。如果所有的promise 都pending,那么Promise.any也会一直pending

对比总结:Promise.allSettled vs Promise.any

特性 Promise.allSettled Promise.any
返回值 一个 Promise,fulfilled 后返回一个数组,数组中每个元素都是一个对象,包含 statusvaluereason 属性。 一个 Promise,如果至少有一个 Promise fulfilled,则返回第一个 fulfilled 的 Promise 的值。如果所有 Promise 都 rejected,则 rejected,并抛出一个 AggregateError 错误。
错误处理 永远不会 rejected,所有错误信息都会包含在返回结果中。 如果所有 Promise 都 rejected,则 rejected,并抛出一个 AggregateError 错误。
适用场景 需要知道所有 Promise 的成功与失败状态,并进行相应的处理。 只需要一个 Promise fulfilled 即可,例如备选方案、服务降级等。
优点 容错性强,不会因为某个 Promise rejected 而中断整个流程。 可以快速返回结果,提高效率。
缺点 需要手动区分 fulfilled 和 rejected,代码相对繁琐。 如果所有 Promise 都 rejected,则会抛出错误,需要进行错误处理。
适用场景例子 监控多个文件上传的状态,需要知道哪些成功,哪些失败。 尝试从多个 CDN 加载资源,只要有一个 CDN 加载成功即可。

实战演练:案例分析

案例一:用户注册 – 邮箱验证 (Promise.any)

用户注册时,我们通常会发送验证邮件。为了提高成功率,我们可以同时使用多个邮件服务提供商 (如 SendGrid, Mailgun, AWS SES)。只要有一个提供商发送成功,就认为验证邮件发送成功。

async function sendVerificationEmail(email, token) {
  const sendGridPromise = sendEmailWithSendGrid(email, token);
  const mailgunPromise = sendEmailWithMailgun(email, token);
  const awsSesPromise = sendEmailWithAwsSes(email, token);

  try {
    await Promise.any([sendGridPromise, mailgunPromise, awsSesPromise]);
    console.log('Verification email sent successfully!');
  } catch (error) {
    console.error('Failed to send verification email with any provider:', error.errors);
    // 可以记录错误日志,并尝试其他方式通知用户
  }
}

案例二:批量数据校验 (Promise.allSettled)

我们需要校验用户提交的批量数据,每个数据项的校验可能依赖于不同的 API。我们需要知道每个数据项的校验结果,以便向用户展示详细的错误信息。

async function validateBatchData(data) {
  const validationPromises = data.map(item => validateDataItem(item)); //validateDataItem返回Promise

  const results = await Promise.allSettled(validationPromises);

  const errors = results.map((result, index) => {
    if (result.status === 'rejected') {
      return {
        index: index,
        error: result.reason,
      };
    }
    return null;
  }).filter(error => error !== null);

  if (errors.length > 0) {
    console.log('Validation errors:', errors);
    // 向用户展示错误信息
  } else {
    console.log('All data items are valid!');
  }
}

高级技巧:结合 async/await

使用 async/await 可以让代码更简洁易懂。

async function processPromises() {
  const results = await Promise.allSettled([
    Promise.resolve(1),
    Promise.reject('Error'),
    Promise.resolve(3),
  ]);

  console.log(results);

  try {
    const value = await Promise.any([
      Promise.reject('Error 1'),
      Promise.resolve(2),
      Promise.reject('Error 3'),
    ]);
    console.log('Fulfilled:', value);
  } catch (error) {
    console.error('All promises rejected:', error.errors);
  }
}

processPromises();

注意事项:

  • Promise.any 是 ES2021 引入的,需要确保运行环境支持。
  • 在使用 Promise.any 时,要 carefully 地处理 AggregateError,获取所有 rejected 的原因。
  • Promise.allSettled 相比 Promise.all,性能上可能会略有损失,因为需要处理所有 Promise 的结果。

总结:

Promise.allSettledPromise.any 都是 Promise 并发控制的重要工具。Promise.allSettled 稳健可靠,适合需要知道所有 Promise 结果的场景。Promise.any 乐观高效,适合只需要一个 Promise 成功的场景。 根据不同的需求,选择合适的工具,才能写出更健壮、更高效的异步代码。

好了,今天的讲座就到这里。希望大家对 Promise.allSettledPromise.any 有了更深入的了解。下次再见!

发表回复

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