Есть ли разница между await Promise.all () и множественным ожиданием?

Ответы:

210

Примечание :

Этот ответ только охватывает временные различия между 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

Это означает, что Promise.allразрешится с данными из внутренних обещаний через 3 секунды.

Но 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вместо этого, вам придется ждать каждого обещания для разрешения последовательно, что может быть не столь эффективно:

Пример № 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

zzzzBov
источник
4
То есть, в основном, разница заключается только в функции Promise.all «быстро проваливаться»?
Мэтью
4
@mclzc В примере # 3 дальнейшее выполнение кода останавливается до разрешения delay1. Это даже в тексте «Если вы используете вместо этого async-await, вам придется ждать, пока каждое обещание будет разрешено последовательно»
Хаггис
1
@Qback, есть живой фрагмент кода, который демонстрирует поведение. Попробуйте запустить его и перечитать код. Вы не первый человек, который неправильно понимает, как ведет себя последовательность обещаний. Ошибка, которую вы допустили в своей демонстрации, заключается в том, что вы не начинаете свои обещания одновременно.
zzzzBov
1
@zzzzBov Ты прав. Вы начинаете это в то же время. Извините, я пришел к этому вопросу по другой причине и не заметил этого.
Qback
2
« это может быть не так эффективно » - и, что более важно, вызывает unhandledrejectionошибки. Вы никогда не захотите использовать это. Пожалуйста, добавьте это к своему ответу.
Берги
88

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

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

mikep
источник
14
этот ответ лучше, чем принятый ответ, потому что в настоящее время принятый ответ пропускает очень важную тему обработки ошибок
chrishiestand
8

Как правило, использование Promise.all()запускает запросы "async" параллельно. Использование awaitможет работать параллельно ИЛИ блокировать синхронизацию.

Функции test1 и test2 ниже показывают, как 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()его также будет «быстрый сбой» - прекратите работу во время первого сбоя любой из включенных функций.

GavinBelson
источник
1
Где я могу получить подробное объяснение того, что происходит под капотом в фрагментах 1 и 2? Я так удивлен, что у них другой способ бега, так как я ожидал, что поведение будет таким же.
Грегорди
2
@ Грегори да, это удивительно. Я опубликовал этот ответ, чтобы сохранить кодеров, плохо знакомых с некоторыми головными болями. Все дело в том, когда JS оценивает ожидание, поэтому важно, как вы назначаете переменные. В глубине Асинхронное
ГэвинБелсон
7

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

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

ZPR
источник
6
На самом деле, ваша скрипка не отвечает на его вопрос. Существует разница между вызовом t1 = task1(); t2 = task2()и последующим использованием awaitдля них обоих, result1 = await t1; result2 = await t2;как в его вопросе, в отличие от того, что вы тестируете, которое использует awaitисходный вызов, как result1 = await task1(); result2 = await task2();. Код в его вопросе запускает все обещания сразу. Разница, как показывает ответ, состоит в том, что сбои будут сообщаться быстрее Promise.all.
BryanGrezeszak
Ваш ответ не по теме, например, @BryanGrezeszak прокомментировал. Вы должны удалить его, чтобы не вводить пользователей в заблуждение.
Микеп
0

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

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

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

Валид Навид
источник