await Promise.all() 和 multiple await 之间有什么区别吗?

2022-08-29 23:56:24

以下两者之间有什么区别吗:

const [result1, result2] = await Promise.all([task1(), task2()]);

const t1 = task1();
const t2 = task2();

const result1 = await t1;
const result2 = await t2;

const [t1, t2] = [task1(), task2()];
const [result1, result2] = [await t1, await t2];

答案 1

注意

这个答案只涵盖了串联和之间的时序差异。请务必阅读@mikep全面的答案,其中还涵盖了错误处理中更重要的差异awaitPromise.all


出于这个答案的目的,我将使用一些示例方法:

  • res(ms)是一个函数,它采用毫秒的整数,并返回在多毫秒后解析的承诺。
  • rej(ms)是一个函数,它采用毫秒的整数,并返回一个在几毫秒后拒绝的承诺。

呼叫启动计时器。使用等待少量延迟将在所有延迟完成后解决,但请记住它们同时执行:resPromise.all

示例 #1
const data = await Promise.all([res(3000), res(2000), res(1000)])
//                              ^^^^^^^^^  ^^^^^^^^^  ^^^^^^^^^
//                               delay 1    delay 2    delay 3
//
// ms ------1---------2---------3
// =============================O delay 1
// ===================O           delay 2
// =========O                     delay 3
//
// =============================O Promise.all

async function example() {
  const start = Date.now()
  let i = 0
  function res(n) {
    const id = ++i
    return new Promise((resolve, reject) => {
      setTimeout(() => {
        resolve()
        console.log(`res #${id} called after ${n} milliseconds`, Date.now() - start)
      }, n)
    })
  }

  const data = await Promise.all([res(3000), res(2000), res(1000)])
  console.log(`Promise.all finished`, Date.now() - start)
}

example()

这意味着这将在3秒后通过内部承诺中的数据解决。Promise.all

但是,Promise.all有一个“快速失败”的行为

示例 #2
const data = await Promise.all([res(3000), res(2000), rej(1000)])
//                              ^^^^^^^^^  ^^^^^^^^^  ^^^^^^^^^
//                               delay 1    delay 2    delay 3
//
// ms ------1---------2---------3
// =============================O delay 1
// ===================O           delay 2
// =========X                     delay 3
//
// =========X                     Promise.all

async function example() {
  const start = Date.now()
  let i = 0
  function res(n) {
    const id = ++i
    return new Promise((resolve, reject) => {
      setTimeout(() => {
        resolve()
        console.log(`res #${id} called after ${n} milliseconds`, Date.now() - start)
      }, n)
    })
  }
  
  function rej(n) {
    const id = ++i
    return new Promise((resolve, reject) => {
      setTimeout(() => {
        reject()
        console.log(`rej #${id} called after ${n} milliseconds`, Date.now() - start)
      }, n)
    })
  }
  
  try {
    const data = await Promise.all([res(3000), res(2000), rej(1000)])
  } catch (error) {
    console.log(`Promise.all finished`, Date.now() - start)
  }
}

example()

如果您改用,则必须等待每个承诺按顺序解析,这可能效率不高:async-await

示例 #3
const delay1 = res(3000)
const delay2 = res(2000)
const delay3 = rej(1000)

const data1 = await delay1
const data2 = await delay2
const data3 = await delay3

// ms ------1---------2---------3
// =============================O delay 1
// ===================O           delay 2
// =========X                     delay 3
//
// =============================X await

async function example() {
  const start = Date.now()
  let i = 0
  function res(n) {
    const id = ++i
    return new Promise((resolve, reject) => {
      setTimeout(() => {
        resolve()
        console.log(`res #${id} called after ${n} milliseconds`, Date.now() - start)
      }, n)
    })
  }
  
  function rej(n) {
    const id = ++i
    return new Promise((resolve, reject) => {
      setTimeout(() => {
        reject()
        console.log(`rej #${id} called after ${n} milliseconds`, Date.now() - start)
      }, n)
    })
  }
  
  try {
    const delay1 = res(3000)
    const delay2 = res(2000)
    const delay3 = rej(1000)

    const data1 = await delay1
    const data2 = await delay2
    const data3 = await delay3
  } catch (error) {
    console.log(`await finished`, Date.now() - start)
  }
}

example()

答案 2

第一个区别 - 快速失败

我同意@zzzzBov的答案,但“快速失败”的优势并不是唯一的区别。评论中的一些用户问为什么使用是值得的,因为它只是在消极的情况下(当某些任务失败时)更快。我问,为什么不呢?如果我有两个独立的异步并行任务,第一个任务需要很长时间才能解决,但第二个任务在很短的时间内被拒绝,为什么让用户等待较长的调用完成以接收错误消息?在实际应用中,我们必须考虑负面情况。但是好的 - 在第一个差异中,您可以决定使用哪种替代方案:vs. 多个 。Promise.allPromise.allPromise.allawait

第二个区别 - 错误处理

但是在考虑错误处理时,必须使用 .无法正确处理由多个 触发的异步并行任务的错误。在消极的情况下,无论你在哪里使用 try/catch,你总是会以 和 结尾。这就是设计的原因。当然,有人会说我们可以抑制这些错误 使用,但这不是好的做法。我在互联网上发现许多例子,它们根本不考虑两个或多个独立的异步并行任务的错误处理,或者考虑它但以错误的方式 - 只是使用try/catch并希望它能捕获错误。几乎不可能在这方面找到好的做法。Promise.allawaitUnhandledPromiseRejectionWarningPromiseRejectionHandledWarningPromise.allprocess.on('unhandledRejection', err => {})process.on('rejectionHandled', err => {})

总结

TL;DR:切勿对两个或多个独立的异步并行任务使用多个 await,因为您将无法正确处理错误。对于此用例,请始终使用 Promise.all()。

Async/不是 Promises 的替代品,它只是一种使用 Promise 的漂亮方式。异步代码是以“同步风格”编写的,我们可以避免在承诺中多次使用。awaitthen

有人说,在使用时,我们不能单独处理任务错误,并且我们只能处理第一个被拒绝的承诺中的错误(单独处理可能很有用,例如用于日志记录)。这不是问题 - 请参阅此答案底部的“添加”标题。Promise.all()

例子

请考虑此异步任务...

const task = function(taskNum, seconds, negativeScenario) {
  return new Promise((resolve, reject) => {
    setTimeout(_ => {
      if (negativeScenario)
        reject(new Error('Task ' + taskNum + ' failed!'));
      else
        resolve('Task ' + taskNum + ' succeed!');
    }, seconds * 1000)
  });
};

在正方案中运行任务时,和多个 s 之间没有区别。两个示例都以 5 秒后结束。Promise.allawaitTask 1 succeed! Task 2 succeed!

// Promise.all alternative
const run = async function() {
  // tasks run immediate in parallel and wait for both results
  let [r1, r2] = await Promise.all([
    task(1, 5, false),
    task(2, 5, false)
  ]);
  console.log(r1 + ' ' + r2);
};
run();
// at 5th sec: Task 1 succeed! Task 2 succeed!
// multiple await alternative
const run = async function() {
  // tasks run immediate in parallel
  let t1 = task(1, 5, false);
  let t2 = task(2, 5, false);
  // wait for both results
  let r1 = await t1;
  let r2 = await t2;
  console.log(r1 + ' ' + r2);
};
run();
// at 5th sec: Task 1 succeed! Task 2 succeed!

但是,当第一个任务需要10秒并成功时,第二个任务需要5秒但失败时,发出的错误存在差异。

// Promise.all alternative
const run = async function() {
  let [r1, r2] = await Promise.all([
      task(1, 10, false),
      task(2, 5, true)
  ]);
  console.log(r1 + ' ' + r2);
};
run();
// at 5th sec: UnhandledPromiseRejectionWarning: Error: Task 2 failed!
// multiple await alternative
const run = async function() {
  let t1 = task(1, 10, false);
  let t2 = task(2, 5, true);
  let r1 = await t1;
  let r2 = await t2;
  console.log(r1 + ' ' + r2);
};
run();
// at 5th sec: UnhandledPromiseRejectionWarning: Error: Task 2 failed!
// at 10th sec: PromiseRejectionHandledWarning: Promise rejection was handled asynchronously (rejection id: 1)
// at 10th sec: UnhandledPromiseRejectionWarning: Error: Task 2 failed!

我们应该已经注意到,在并行使用多个 s 时,我们做错了什么。让我们尝试处理错误:await

// Promise.all alternative
const run = async function() {
  let [r1, r2] = await Promise.all([
    task(1, 10, false),
    task(2, 5, true)
  ]);
  console.log(r1 + ' ' + r2);
};
run().catch(err => { console.log('Caught error', err); });
// at 5th sec: Caught error Error: Task 2 failed!

如您所见,要成功处理错误,我们只需要向函数添加一个 catch,并将具有 catch 逻辑的代码添加到回调中。我们不需要处理函数内部的错误,因为异步函数会自动执行此操作 - 承诺拒绝函数会导致函数被拒绝。runruntaskrun

为了避免回调,我们可以使用“sync style”(async/ + try/ catch
),
但在此示例中这是不可能的,因为我们不能在主线程中使用 - 它只能在异步函数中使用(因为没有人想要阻塞主线程)。为了测试处理是否以“同步风格”工作,我们可以从另一个异步函数调用该函数或使用IIFE(立即调用的函数表达式:MDN):awaittry { await run(); } catch(err) { }awaitrun

(async function() { 
  try { 
    await run(); 
  } catch(err) { 
    console.log('Caught error', err); 
  }
})();

这是运行两个或多个异步并行任务和处理错误的唯一正确方法。您应该避免以下示例。

不好的例子

// multiple await alternative
const run = async function() {
  let t1 = task(1, 10, false);
  let t2 = task(2, 5, true);
  let r1 = await t1;
  let r2 = await t2;
  console.log(r1 + ' ' + r2);
};

我们可以尝试以多种方式处理上述代码中的错误...

try { run(); } catch(err) { console.log('Caught error', err); };
// at 5th sec: UnhandledPromiseRejectionWarning: Error: Task 2 failed!
// at 10th sec: UnhandledPromiseRejectionWarning: Error: Task 2 failed!
// at 10th sec: PromiseRejectionHandledWarning: Promise rejection was handled 

...没有捕获任何内容,因为它处理同步代码,但已异步。run

run().catch(err => { console.log('Caught error', err); });
// at 5th sec: UnhandledPromiseRejectionWarning: Error: Task 2 failed!
// at 10th sec: Caught error Error: Task 2 failed!
// at 10th sec: PromiseRejectionHandledWarning: Promise rejection was handled asynchronously (rejection id: 1)

...哼?我们首先看到任务 2 的错误未得到处理,后来又被捕获。具有误导性并且控制台中仍然充满错误,但以这种方式仍然无法使用。

(async function() { try { await run(); } catch(err) { console.log('Caught error', err); }; })();
// at 5th sec: UnhandledPromiseRejectionWarning: Error: Task 2 failed!
// at 10th sec: Caught error Error: Task 2 failed!
// at 10th sec: PromiseRejectionHandledWarning: Promise rejection was handled asynchronously (rejection id: 1)

...同上。用户@Qwerty在他删除的答案中询问了这种奇怪的行为,其中错误似乎被捕获,但也未处理。我们捕获错误,因为在带有关键字的行上被拒绝,并且可以在调用时使用try/ catch捕获。我们还会收到一个未处理的错误,因为我们同步调用异步任务函数(不带关键字),并且此任务在函数外部运行并失败。
这类似于当我们在调用一些调用 setTimeout 的 sync 函数时,无法通过 try/catch 来处理错误:run()awaitrun()awaitrun()

function test() {
  setTimeout(function() { 
    console.log(causesError); 
    }, 0);
}; 
try { 
  test(); 
} catch(e) { 
  /* this will never catch error */ 
}`.

另一个糟糕的例子:

const run = async function() {
  try {
    let t1 = task(1, 10, false);
    let t2 = task(2, 5, true);
    let r1 = await t1;
    let r2 = await t2;
  }
  catch (err) {
    return new Error(err);
  }
  console.log(r1 + ' ' + r2);
};
run().catch(err => { console.log('Caught error', err); });
// at 5th sec: UnhandledPromiseRejectionWarning: Error: Task 2 failed!
// at 10th sec: PromiseRejectionHandledWarning: Promise rejection was handled asynchronously (rejection id: 1)

...“仅”两个错误(缺少第三个错误),但未捕获任何内容。

加法(处理单独的任务错误和首次失败错误)

const run = async function() {
  let [r1, r2] = await Promise.all([
    task(1, 10, true).catch(err => { console.log('Task 1 failed!'); throw err; }),
    task(2, 5, true).catch(err => { console.log('Task 2 failed!'); throw err; })
  ]);
  console.log(r1 + ' ' + r2);
};
run().catch(err => { console.log('Run failed (does not matter which task)!'); });
// at 5th sec: Task 2 failed!
// at 5th sec: Run failed (does not matter which task)!
// at 10th sec: Task 1 failed!

...请注意,在此示例中,我拒绝了这两个任务,以更好地演示发生的情况(用于触发最终错误)。throw err