Любая разница между ожиданием Promise.all() и несколькими ожиданиями?

Есть ли разница между:

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

Note:

Этот ответ охватывает только временные различия между сериями await и Promise.all. Обязательно прочитайте @mikep исчерпывающий ответ, который также охватывает более важные различия в обработке ошибок.


В этом ответе я буду использовать несколько примеров методов:

  • res(ms) - это функция, которая принимает целое число миллисекунд и возвращает обещание, которое разрешается через столько миллисекунд.
  • rej(ms) - это функция, которая принимает целое число миллисекунд и возвращает обещание, которое отклоняется через столько миллисекунд.

Вызов 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()

Ответ 2

Первое отличие - быстро провалиться

Я согласен с ответом @zzzzBov, но преимущество Promise в "быстром провале" - не единственное отличие. Некоторые пользователи в комментариях спрашивают, зачем использовать Promise.all, когда это быстрее только в отрицательном сценарии (когда какая-то задача не выполняется). И я спрашиваю, почему нет? Если у меня есть две независимые асинхронные параллельные задачи, и первая решается за очень долгое время, а вторая отклоняется за очень короткое время, зачем оставлять пользователю ждать сообщения об ошибке "очень долгое время" вместо "очень короткое время"? В реальных приложениях мы должны учитывать негативный сценарий. Но хорошо - в этом первом разнице вы можете решить, какую альтернативу использовать Promise.all вместо нескольких ожидающих.

Второе отличие - обработка ошибок

Но при рассмотрении обработки ошибок ВЫ ДОЛЖНЫ использовать Promise.all. Невозможно правильно обрабатывать ошибки асинхронных параллельных задач, вызванных множественным ожиданием. В негативном сценарии вы всегда заканчиваете UnhandledPromiseRejectionWarning и PromiseRejectionHandledWarning, хотя вы используете try/catch где угодно. Вот почему Promise.all был разработан. Конечно, кто-то может сказать, что мы можем устранить эти ошибки, используя process.on('unhandledRejection', err => {}) и process.on('rejectionHandled', err => {}), но это не очень хорошая практика. Я нашел много примеров в интернете, которые вообще не рассматривают обработку ошибок для двух или более независимых асинхронных параллельных задач или рассматривают это, но неправильно - просто используя try/catch и надеясь, что он поймает ошибки. Практически невозможно найти хорошую практику. Вот почему я пишу этот ответ.

Резюме

Никогда не используйте множественное ожидание для двух или более независимых асинхронных параллельных задач, потому что вы не сможете серьезно обрабатывать ошибки. Всегда используйте Promise.all() для этого варианта использования. Async/await не является заменой для Promises. Это просто прекрасный способ использовать обещания... асинхронный код написан в стиле синхронизации, и мы можем избежать многократного then в обещаниях.

Некоторые люди говорят, что с помощью 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)
  });
};

При выполнении задач в положительном сценарии нет разницы между Promise.all и множественным ожиданием. Оба примера заканчиваются на 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!

Мы уже должны заметить, что мы делаем что-то не так при параллельном использовании нескольких ожидающих. Конечно, чтобы избежать ошибок, мы должны справиться с этим! Давай попробуем...


// 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, и код с логикой перехвата находится в обратном вызове (асинхронный стиль). Нам не нужно обрабатывать ошибки внутри функции run, потому что асинхронная функция выполняет это автоматически - обещание отклонения функции task вызывает отклонение функции run. Чтобы избежать обратного вызова, мы можем использовать стиль синхронизации (async/await + try/catch) try { await run(); } catch(err) { }, но в этом примере это невозможно, потому что мы не можем использовать await в основном потоке - его можно использовать только в асинхронной функции (это логично, потому что никто не хочет блокировать основной поток). Чтобы проверить, работает ли обработка в стиле синхронизации, мы можем вызвать функцию run из другой асинхронной функции или использовать IIFE (выражение немедленного вызова функции): (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)

... Wtf? Во-первых, мы видим, что ошибка для задачи 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(), а также выходит из строя снаружи. Это похоже на случай, когда мы не можем обработать ошибку путем 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!

... обратите внимание, что в этом примере я использовал отрицательный сценарий = true для обеих задач для лучшей демонстрации того, что происходит (throw err используется для запуска окончательной ошибки)

Ответ 3

Вы можете проверить сами.

В этой скрипке я запустил тест, чтобы продемонстрировать блокирующий характер await, в отличие от Promise.all, который запустит все обещания, и пока один ожидает, он продолжит выполнение других.

Ответ 4

В случае await Promise.all([task1(), task2()]); "task1()" и "task2()" будут работать параллельно и будут ждать, пока оба обещания будут выполнены (либо решены, либо разрешены). или отклонено). В то время как в случае

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

t2 будет работать только после того, как t1 завершит выполнение (было разрешено или отклонено). И t1, и t2 не будут работать параллельно.