Есть ли разница между await 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];

7 ответов

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

  • 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()

Это означает, что 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 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()

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

Я согласен с ответом @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!

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

... так же, как и выше.

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!

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

Обычно, используя Promise.all()параллельно выполняет асинхронные запросы. С помощью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
};

TL; DR; Если вы используетеPromise.all()он также будет "быстро выходить из строя" - прекращать работу в момент первого отказа любой из включенных функций.

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

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

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

В этом случае task1 и task2 будут выполняться параллельно, и как только ответ поступит от обеих функций, обещание будет разрешено и сохранено в результате 1 и 2.

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

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

Здесь, когда вызывается функция t1, код блокируется на этом этапе, пока ответ не вернется. Так что это будет довольно медленно, чем первый пример.

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

Здесь также вызовы не будут выполняться параллельно, они будут выполняться один за другим.

Так что лучший подход - это использовать обещание. Это самый быстрый из трех.

На всякий случай, в дополнение к уже потрясающим ответам:

Возможный результат:

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

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

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

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

Другие вопросы по тегам