Будь-яка різниця між програмою очікування Promise.all () та кількома очікуванням?


181

Чи є різниця між:

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

Відповіді:


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


4
Отже, в основному різниця полягає лише в функції "невдалого виходу з" Promise.all?
Матвій

4
@mclzc У прикладі №3 подальше виконання коду припиняється, поки не вирішиться затримка1. Це навіть в тексті «Якщо ви використовуєте асінхра-ждет замість цього, вам доведеться чекати кожну обіцянку вирішити послідовно»
хаггис

1
@Qback, є фрагмент живого коду, який демонструє поведінку. Поміркуйте запустити його і перечитати код. Ви не перша людина, яка неправильно зрозуміла, як поводиться послідовність обіцянок. Помилка, яку ви зробили в демонстрації, полягає в тому, що ви не починаєте обіцянок одночасно.
zzzzBov

1
@zzzzBov Ви маєте рацію. Ви починаєте це в той же час. Вибачте, що я прийшов до цього питання з іншої причини, і це я не помітив.
Qback

2
" це може бути не настільки ефективно " - і що ще важливіше, викликати unhandledrejectionпомилки. Ви ніколи не захочете цим користуватися. Будь ласка, додайте це до своєї відповіді.
Берги

88

Перша різниця - швидко провалитися

Я погоджуюся з відповіддю @ zzzzBov, але перевага Promise.all - не лише одна різниця. Деякі користувачі в коментарях запитують, навіщо використовувати Promise.all, коли це лише швидше за негативного сценарію (коли деякі завдання не вдається). І я запитую, чому ні? Якщо у мене є дві незалежні паралельні задачі асинхронізації, і перша вирішується за дуже довгий час, але друга відхиляється за дуже короткий час, чому слід залишати користувачеві чекати повідомлення про помилку "дуже довго" замість "дуже короткого часу"? У реальних програмах ми повинні враховувати негативний сценарій. Але добре - у цій першій різниці ви можете вирішити, яку альтернативу використовувати Promise.all проти кількох очікуючих.

Друга відмінність - обробка помилок

Але при розгляді помилок з ВАС МОЖЕТЕ використовувати Promise.all. Неможливо правильно впоратися з помилками асинхронізації паралельних завдань, спровокованих багаторазовим очікуванням. У негативному сценарії ви завжди будете закінчуватися, UnhandledPromiseRejectionWarningі PromiseRejectionHandledWarningхоча ви використовуєте пробувати / ловити будь-де. Саме тому Promise.all був розроблений. Звичайно , хто - то може сказати , що ми можемо придушити , що помилки з допомогою process.on('unhandledRejection', err => {})і , process.on('rejectionHandled', err => {})але це не є доброю практикою. В Інтернеті я знайшов багато прикладів, які взагалі не розглядають помилки для двох чи більше незалежних паралельних завдань асинхронізації або розглядають її, але неправильно - просто використовую спробу / ловити та сподіваюся, що вона виявить помилки. Практичну практику знайти майже неможливо. Ось чому я пишу цю відповідь.

Підсумок

Ніколи не використовуйте кілька функцій очікування для двох або більше незалежних завдань паралельної асинхронізації, оскільки ви не зможете серйозно обробляти помилки. Завжди використовуйте Promise.all () для цього випадку використання. Асинхронізація / очікування - це не заміна обіцянок. Це просто гарний спосіб використання обіцянок ... Асинхронний код написаний у стилі синхронізації, і ми можемо уникнути декількох 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в основному потоці - він може використовуватися лише у функції async (це логічно, тому що ніхто не хоче блокувати головну нитку). Щоб перевірити, чи працює обробка в стилі синхронізації, ми можемо зателефонувати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 () відхиляється в рядку з ключовим словом очікуванням і може бути перехоплена за допомогою try / catch при виклику run (). Ми також отримуємо необроблену помилку, оскільки ми викликаємо функцію завдання async синхронно (без очікування ключового слова), і це завдання працює поза функцією 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)

... "лише" дві помилки (3-я одна відсутня), але нічого не застало.


Доповнення (обробляти помилки завдання окремо, а також помилку, що не відбулася)

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!

... зауважте, що в цьому прикладі я використовував negativeScenario = true для обох завдань для кращої демонстрації того, що відбувається ( throw errвикористовується для запуску остаточної помилки)


14
ця відповідь краще, ніж прийнята відповідь, тому що в даний час прийнято відповідь пропускає дуже важливу тему поводження з помилками
Христос

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


1
Де я можу отримати детальне пояснення того, що відбувається під кришкою у фрагментах 1 і 2? Я настільки здивований, що у них є інший спосіб бігати, як я очікував, що поведінка буде однаковою.
Грегорді

2
@Gregordy так, це дивно. Я опублікував цю відповідь, щоб зберегти нові кодери, щоб асинхронізувати деякі головні болі. Вся справа в тому, коли JS оцінює очікування, ось чому питання призначення змінних має значення. Поглиблене читання Async: blog.bitsrc.io/…
GavinBelson

7

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

У цій скрипці я провів тест, щоб продемонструвати блокуючу природу await, на відміну від Promise.allякої розпочнеться всі обіцянки, і поки один чекає, він продовжуватиметься з іншими.


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

Ваша відповідь поза темою, наприклад коментар @BryanGrezeszak. Ви повинні скоріше видалити його, щоб не вводити в оману користувачів.
mikep

0

У разі очікування Promise.all ([task1 (), task2 ()]); "task1 ()" і "task2 ()" будуть працювати паралельно і будуть чекати, поки обидва обіцянки будуть виконані (або вирішені, або відхилені). В той час, як у випадку

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

t2 запускається лише після завершення виконання t1 (було вирішено або відхилено). І t1, і t2 не будуть працювати паралельно.

Використовуючи наш веб-сайт, ви визнаєте, що прочитали та зрозуміли наші Політику щодо файлів cookie та Політику конфіденційності.
Licensed under cc by-sa 3.0 with attribution required.