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

IT技术 javascript async-await
2021-02-03 15:21:56

之间有什么区别:

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];
5个回答

注意

该答案仅涵盖await串联和Promise.all. 请务必阅读@mikep 的综合答案,其中还涵盖了错误处理中更重要的差异


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

  • res(ms) 是一个函数,它接受一个整数毫秒并返回一个Promise,该Promise在许多毫秒后解析。
  • rej(ms) 是一个函数,它接受一个整数毫秒并返回一个Promise,该Promise在这么多毫秒后拒绝。

呼叫res启动计时器。使用Promise.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

这意味着Promise.all将在 3 秒后使用来自内部Promise的数据进行解析。

但是,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-await改为使用,则必须等待每个 promise 依次解析,这可能效率不高:

示例 #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

它可能没有那么高效”——更重要的是,会导致unhandledrejection错误。你永远不会想要使用它。请将此添加到您的答案中。
2021-03-11 15:21:56
所以关于一切都成功的成功案例,没有Promise.all()await背靠背做3更快的案例,对吗?所以Promise.all()出于性能原因是没用的,除了快速失败的场景?
2021-03-15 15:21:56
@mclzc 在示例 #3 中,进一步的代码执行将暂停,直到 delay1 解决。它甚至在文本“如果您使用 async-await 代替,您将不得不等待每个Promise按顺序解决”
2021-03-21 15:21:56
所以基本上区别只是 Promise.all 的“快速失败”功能?
2021-04-01 15:21:56
@HenriLapierre,我见过太多开发人员错误地执行串行await(即data1 = await thing1(); data2 = await thing2(); data3 = await thing3();)认为他们正在并行运行Promise。所以回答你的问题,如果你的Promise已经开始,他们就无法更快地解决我不知道为什么你会认为它们可以通过Promise.all().
2021-04-03 15:21:56

第一个区别 - 快速失败

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

第二个区别 - 错误处理

但是在考虑错误处理时,您必须使用Promise.all. 无法正确处理由多个awaits触发的异步并行任务的错误在消极的情况下,无论您在何处使用 try/catch,您将始终以UnhandledPromiseRejectionWarningand结尾PromiseRejectionHandledWarning这就是为什么Promise.all被设计。当然有人会说我们可以使用process.on('unhandledRejection', err => {})and来抑制这些错误process.on('rejectionHandled', err => {})但这不是一个好习惯。我在 Internet 上发现了许多示例,它们根本不考虑对两个或多个独立异步并行任务进行错误处理,或者以错误的方式考虑它——只是使用 try/catch 并希望它会捕获错误。在这方面几乎不可能找到好的做法。

概括

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

Async/await不是 Promises 的替代品,它只是使用 Promises 的一种很好的方式。异步代码以“同步风格”编写,我们可以避免then在 Promise 中出现多个s。

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

例子

考虑这个异步任务......

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)
  });
};

当您在积极的场景中运行任务时Promise.all,多个awaits之间没有区别两个示例都Task 1 succeed! Task 2 succeed!在 5 秒后结束

// 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!

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

// 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!

如您所见,为了成功处理错误,我们只需要在run函数中添加一个 catch并将带有 catch 逻辑的代码添加到回调中。我们不需要处理run函数内部的错误,因为异步函数会自动执行此操作——Promise拒绝task函数会导致拒绝run函数。

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

(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 在他删除的答案中询问了这种奇怪的行为,其中错误似乎被捕获但也未处理。我们捕获错误,因为run()await关键字行上被拒绝,并且可以在调用时使用 try/catch 捕获run()我们还得到一个未处理的错误,因为我们正在同步调用一个异步任务函数(没有await关键字),并且这个任务在run()函数外运行并失败
这类似于在调用某些调用 setTimeout 的同步函数时,我们无法通过 try/catch 处理错误:

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用于触发最终错误)。

这个答案比接受的答案更好,因为当前接受的答案错过了错误处理这个非常重要的话题
2021-03-27 15:21:56
无需提及线程,它们都在单个线程中运行。并发不是并行
2021-03-31 15:21:56

通常,使用Promise.all()并行运行请求“异步”。使用await可以并行运行“同步”阻塞。

下面的test1test2函数显示了如何await运行异步或同步。

test3显示Promise.all()这是异步的。

带有定时结果的 jsfiddle - 打开浏览器控制台查看测试结果

同步行为。不并行运行,需要约1800 毫秒

const test1 = async () => {
  const delay1 = await Promise.delay(600); //runs 1st
  const delay2 = await Promise.delay(600); //waits 600 for delay1 to run
  const delay3 = await Promise.delay(600); //waits 600 more for delay2 to run
};

异步行为。并行运行,大约需要600 毫秒

const test2 = async () => {
  const delay1 = Promise.delay(600);
  const delay2 = Promise.delay(600);
  const delay3 = Promise.delay(600);
  const data1 = await delay1;
  const data2 = await delay2;
  const data3 = await delay3; //runs all delays simultaneously
}

异步行为。并行运行,大约需要600 毫秒

const test3 = async () => {
  await Promise.all([
  Promise.delay(600), 
  Promise.delay(600), 
  Promise.delay(600)]); //runs all delays simultaneously
};

TLDR;如果您正在使用Promise.all()它,它也会“快速失败”——在任何包含的功能第一次失败时停止运行

我在哪里可以获得有关片段 1 和片段 2 中幕后情况的详细说明?我很惊讶这些有不同的运行方式,因为我期望行为是相同的。
2021-03-13 15:21:56
如果您只有少数异步函数或者它们非常快,那么完成时间的差异可以忽略不计。但是如果你有数百个,不使用 Promise.all 将是对资源的极大浪费,因为事件循环将按顺序处理它们(除非你需要它们)。尝试在 async/await 中构建网络爬虫,您会看到问题
2021-03-25 15:21:56
@Gregordy 是的,这令人惊讶。我发布了这个答案,以节省异步编码新手的麻烦。这完全是关于 JS 何时评估等待,这就是为什么分配变量很重要的原因。深度异步阅读:blog.bitsrc.io/...
2021-03-26 15:21:56
在片段 1 中,您可以使用 try..catch 优雅地处理错误。在片段 2 中,您遇到了@mikep 提到的未处理的Promise拒绝问题
2021-03-29 15:21:56

你可以自己查一下。

在这个小提琴中,我运行了一个测试来演示 的阻塞性质await,而不是Promise.all它会启动所有的Promise,而当一个人在等待时,它会继续其他人。

您的回答与@BryanGrezeszak 评论的主题无关。您应该删除它以避免误导用户。
2021-03-16 15:21:56
它的题外话。但这可能有助于某人更好地理解,它帮助了我
2021-03-22 15:21:56
实际上,您的小提琴并没有解决他的问题。就像在他的问题中一样,调用t1 = task1(); t2 = task2()之后使用await它们之间存在差异result1 = await t1; result2 = await t2;,而不是您正在测试await的原始调用中使用的内容,例如result1 = await task1(); result2 = await task2();. 他问题中的代码确实立即启动了所有Promise。不同之处,如答案所示,失败将更快地得到报告Promise.all
2021-03-30 15:21:56

如果是await Promise.all([task1(), task2()]); “task1()”和“task2()”将并行运行并等待两个promise完成(解决或拒绝)。而在情况下

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

t2 只会在 t1 完成执行(已解决或拒绝)后运行。t1 和 t2 不会并行运行。