await Promise.all() 和多个 await 之间有什么区别?

383

两者之间是否有任何区别:

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];

紧密相关:等待多个并发 await 操作 - Bergi
2
这个问题对我来说非常有价值。在这里学到了很多。 - Rafe
6个回答

390
注意:这个答案只涵盖了await在串行和Promise.all之间的时间差异。请务必阅读@mikep的全面回答,其中还涵盖了更重要的错误处理差异。
为了回答这个问题,我将使用一些示例方法:
- `res(ms)` 是一个函数,它接受一个以毫秒为单位的整数,并返回一个在指定毫秒数后解析的 Promise。 - `rej(ms)` 是一个函数,它接受一个以毫秒为单位的整数,并返回一个在指定毫秒数后拒绝的 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

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,你将不得不等待每个promise按顺序解决,这可能不是很高效。
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()


21
基本上,Promise.all 的区别就在于其具有“快速失败”功能是吗? - Matthew
3
关于成功情况下一切顺利的案例,没有情况Promise.all()比连续使用3个await更快,这是正确的吗?因此出于性能原因使用Promise.all()是无用的,除非是针对失败快速返回的情况。 - HLP
3
@HenriLapierre,我看到太多开发人员犯一个错误,就是连续执行await(例如data1 = await thing1(); data2 = await thing2(); data3 = await thing3();),以为它们同时运行了承诺。因此,回答你的问题,如果您的承诺已经开始,它们不能被加速。我不知道为什么您认为Promise.all()可以加速它们。 - zzzzBov
7
在示例3中,代码执行会在delay1解析之前停止。甚至在文本中也有这样的描述:“如果你使用async-await,你将不得不等待每个promise按顺序解决”。 - haggis
5
它可能不如效率高”,更重要的是会引起unhandledrejection错误。你绝对不想使用它。请将此添加到您的答案中。 - Bergi
显示剩余13条评论

296

第一个区别 - 快速失败

我同意@zzzzBov的答案,但Promise.all的“快速失败”优势并不是唯一的区别。在评论中,一些用户问为什么在负面情况下(当某些任务失败时)使用Promise.all要比其他方式值得。而我想问:为什么不呢?如果我有两个独立的异步并行任务,第一个任务需要很长时间才能完成,而第二个任务在很短的时间内被拒绝了,为什么要让用户等待更长时间的调用才能收到错误信息?在现实生活中的应用程序中,我们必须考虑负面情况。但好吧,在这个第一个区别中,您可以决定使用哪种替代方案:Promise.all还是多个await

第二个区别 - 错误处理

但在考虑错误处理时,必须使用Promise.all。使用多个await触发的异步并行任务无法正确处理错误。在负面情况下,无论您在哪里使用try/catch,您都将以UnhandledPromiseRejectionWarningPromiseRejectionHandledWarning结束。这就是为什么会设计Promise.all的原因。当然,有人可能会说,我们可以使用process.on('unhandledRejection', err => {})process.on('rejectionHandled', err => {})来抑制这些错误,但这不是好的做法。我在互联网上找到了许多根本没有考虑两个或更多独立异步并行任务的错误处理,或者考虑了但以错误的方式处理错误 - 只使用try/catch并希望它能捕获错误。几乎不可能找到这方面的良好实践。

摘要

TL;DR: 不要为两个或更多独立异步并行任务使用多个await,因为您将无法正确处理错误。对于这种情况,请始终使用Promise.all()

Async/await不能替代Promises,它只是一种使用promises的漂亮方式。异步代码以“同步样式”编写,我们可以避免在Promises中使用多个then

有些人说,在使用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和多个await之间没有区别。这两个示例都在5秒后以 Task 1 succeed! Task 2 succeed! 结束。

// Promise.all alternative
const run = async function() {
  // tasks run immediately 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 immediately 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时,我们正在做错什么。让我们尝试处理这些错误:

// 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函数内部处理错误,因为异步函数会自动处理 - task函数的Promise拒绝会导致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 关键字的行上被拒绝,可以在调用 run() 时使用 try/catch 捕获。我们还会出现一个 未处理 的错误,因为我们在同步地调用异步任务函数(没有 await 关键字),并且此任务在 run() 函数之外运行和失败。
这类似于当我们无法通过 try/catch 处理调用某些同步函数调用 setTimeout 时的错误:

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


38
这个答案比被采纳的答案更好,因为目前被采纳的答案忽略了非常重要的错误处理主题。 - chrishiestand
2
并发不等于并行。请参考Concurrency is not Parallelism视频。 - Kutalia
这个问题值得作为错误处理的一个独立话题。我只是想问一下系统是如何生成这些警告的,是否有全局监听器或者类似于Promise拒绝的包装器,还是Promise被异步处理? - Xiaoye Yang
1
@sleighty 这个话题很复杂,所以你必须提供一个简短的例子来说明你的想法。这个 try { let r1 = await task(1); let r2 = await task(2); } catch (err) {} 串行地运行任务,并且处理方式是这样的。这个 try { let t1 = task(1); let t2 = task(2); let r1 = await t1; let r2 = await t2; } catch (err) {} 并行地运行任务,但你没有捕获到任何东西 - 处理方式不是这样的,因为 try/catch 是用于同步代码的,但在调用任务之前省略 await 导致代码在后台异步失败。所有的内容都在我的回答中的例子里了。 - mikep
1
@mikep 我明白你的意思了!我不知道它会在后台异步失败。感谢你提供的例子。 - sleighty
显示剩余2条评论

59

一般来说,使用Promise.all()以“异步”方式并行运行请求。而使用await则可以同时运行并行处理是“同步”阻塞。

test1test2 函数展示了await如何异步或同步运行。

test3 展示了异步Promise.all()

jsfiddle with timed results - 打开浏览器控制台查看测试结果

同步行为,不会并行运行,需要 ~1800ms

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

简而言之,如果您正在使用Promise.all(),它也会"快速失败"——在任何一个包含的函数第一次失败时停止运行。


13
我在哪里可以获得有关代码片段1和2内部运行机制的详细解释?我很惊讶它们有不同的运行方式,因为我原本以为它们的行为是相同的。 - Gregordy
3
@Gregordy 是的,这很令人惊讶。我发布这个答案是为了帮助那些刚接触异步编程的程序员避免一些麻烦。重点在于JS何时评估 await,这就是为什么变量分配方式很重要的原因。深入了解异步内容请查看:https://blog.bitsrc.io/understanding-javascript-async-and-await-with-examples-a010b03926ea - GavinBelson
在片段1中,您可以使用try..catch优雅地处理错误。在片段2中,您遇到了@mikep提到的未处理的Promise拒绝问题。 - Luke H
如果您只有少量的异步函数或它们非常快,那么完成时间的差异将是可以忽略不计的。但是,如果您有数百个这样的函数,则不使用Promise.all将是一种极大的资源浪费,因为事件循环将按顺序处理它们(除非您需要它们)。尝试使用async/await构建Web爬虫,您会看到问题。 - maksbd19

9
你可以自行检查。
在这个演示中,我运行了一个测试来展示await的阻塞性质,与Promise.all相比,后者会启动所有的promise,并在等待其中一个promise时继续执行其他promise。

9
实际上,你的代码并没有回答他的问题。在他的问题中,调用t1 = task1(); t2 = task2(),然后再使用await来获取两个任务的结果result1 = await t1; result2 = await t2;与你所测试的方式是不同的,你测试的方式是直接在原始调用上使用await,像这样result1 = await task1(); result2 = await task2();。他的问题中的代码确实同时启动了所有的promise。正如答案所展示的那样,区别在于使用Promise.all的方式可以更快地报告失败信息。 - BryanGrezeszak
你的回答与主题无关,就像@BryanGrezeszak所评论的那样。你应该删除它以避免误导用户。 - mikep
1
这个话题有些偏离主题,但可能会帮助某些人更好地理解,它也帮助了我。 - some_groceries
fiddle很有帮助!顺便提一下,在你的Promise.all中最后一个Promise.delay传递了000,但它并没有影响结果。 - J.E.C.

0

仅供参考,除了已经很棒的答案:

const rejectAt = 3;

// No worries. "3" is purely awesome, too! Just for the tiny example!

document.body.innerHTML = '';
o("// With 'Promise.all()':");

let a = Promise.all([
    test(1),
    test(2),
    test(3),
    test(4),
    test(5),
]).then(v => {
    o(`+ Look! We got all: ${v}`);
}).catch(e => {
    o(`x Oh! Got rejected with '${e}'`);
}).finally(() => {
    o("\n// With 'await':");

    async function test2() {
        try {
            r = [];
            r.push(await test(1));
            r.push(await test(2));
            r.push(await test(3));
            r.push(await test(4));
            r.push(await test(5));
            o(`+ Look! We got all: ${r.join(',')} // Twice as happy! ^^`);
        } catch (e) {
            o(`x Ah! Got rejected with '${e}'`);
        }
    }

    test2();
});

function test(v) {
    if (v === rejectAt) {
        o(`- Test ${v} (reject)`);
        return new Promise((undefined, reject) => reject(v));
    }

    o(`- Test ${v} (resolve)`);
    return new Promise((resolve, undefined) => resolve(v));
}

// ----------------------------------------

// Output
function o(value) {
    document.write(`${value}\n`);
}
body {
  white-space: pre;
  font-family: 'monospace';
}

一个可能的结果:
// With 'Promise.all()':
- Test 1 (resolve)
- Test 2 (resolve)
- Test 3 (reject)
- Test 4 (resolve)
- Test 5 (resolve)
x Oh! Got rejected with '3'

// With 'await':
- Test 1 (resolve)
- Test 2 (resolve)
- Test 3 (reject)
x Ah! Got rejected with '3'

0
await Promise.all([task1(), task2()]); 的情况下,"task1()" 和 "task2()" 将并行运行,并等待两个 promise 完成(无论是 resolved 还是 rejected)。而在其他情况下,则不会。
const result1 = await t1;
const result2 = await t2;

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


网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接