Gibt es einen Unterschied zwischen dem Warten auf Promise.all () und dem Mehrfachen Warten?

179

Gibt es einen Unterschied zwischen:

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

und

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

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

und

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

Antworten:

208

Hinweis :

Diese Antwort deckt nur die zeitlichen Unterschiede zwischen awaitin Serie und ab Promise.all. Lesen Sie unbedingt die umfassende Antwort von @ mikep, die auch die wichtigeren Unterschiede bei der Fehlerbehandlung abdeckt .


Für die Zwecke dieser Antwort werde ich einige Beispielmethoden verwenden:

  • res(ms) ist eine Funktion, die eine Ganzzahl von Millisekunden benötigt und ein Versprechen zurückgibt, das nach so vielen Millisekunden aufgelöst wird.
  • rej(ms) ist eine Funktion, die eine Ganzzahl von Millisekunden benötigt und ein Versprechen zurückgibt, das nach so vielen Millisekunden abgelehnt wird.

Der Anruf resstartet den Timer. Wenn Sie Promise.allauf eine Handvoll Verzögerungen warten, wird das Problem behoben, nachdem alle Verzögerungen abgeschlossen sind. Denken Sie jedoch daran, dass sie gleichzeitig ausgeführt werden:

Beispiel 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

Dies bedeutet, dass Promise.allmit den Daten aus den inneren Versprechungen nach 3 Sekunden aufgelöst wird.

Hat Promise.allaber ein "schnelles Versagen" -Verhalten :

Beispiel 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

Wenn Sie async-awaitstattdessen verwenden, müssen Sie warten, bis jedes Versprechen nacheinander aufgelöst wird. Dies ist möglicherweise nicht so effizient:

Beispiel 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
quelle
4
Der Unterschied besteht also im Grunde nur in der Funktion "Schnell ausfallen" von Promise.all?
Matthew
4
@mclzc In Beispiel 3 wird die weitere Codeausführung angehalten, bis delay1 aufgelöst wird. Es ist sogar in dem Text „Wenn Sie Asynchron-warten stattdessen verwenden, werden Sie für jedes Versprechen warten müssen , um der Reihe nach zu lösen“
Haggis
1
@Qback, es gibt ein Live-Code-Snippet, das das Verhalten demonstriert. Führen Sie es aus und lesen Sie den Code erneut. Sie sind nicht die erste Person, die falsch versteht, wie sich die Reihenfolge der Versprechen verhält. Der Fehler, den Sie in Ihrer Demo gemacht haben, ist, dass Sie Ihre Versprechen nicht gleichzeitig beginnen.
zzzzBov
1
@zzzzBov Du hast recht. Du fängst es zur gleichen Zeit an. Entschuldigung, ich bin aus einem anderen Grund auf diese Frage gekommen und habe das übersehen.
Qback
2
" Es ist möglicherweise nicht so effizient " - und vor allem unhandledrejectionFehler verursachen . Sie werden dies niemals verwenden wollen. Bitte fügen Sie dies Ihrer Antwort hinzu.
Bergi
85

Erster Unterschied - schnell scheitern

Ich stimme der Antwort von @ zzzzBov zu, aber der Vorteil von Promise.all ist nicht nur der einzige Unterschied. Einige Benutzer in Kommentaren fragen, warum Promise.all verwendet werden soll, wenn es nur im negativen Szenario schneller ist (wenn eine Aufgabe fehlschlägt). Und ich frage warum nicht? Wenn ich zwei unabhängige asynchrone parallele Aufgaben habe und die erste in sehr langer Zeit gelöst wird, die zweite jedoch in sehr kurzer Zeit abgelehnt wird, warum sollte der Benutzer auf die Fehlermeldung "sehr lange Zeit" anstatt "sehr kurze Zeit" warten? In realen Anwendungen müssen wir ein negatives Szenario berücksichtigen. Aber OK - in diesem ersten Unterschied können Sie entscheiden, welche Alternative Promise.all oder mehrere verwenden sollen.

Zweiter Unterschied - Fehlerbehandlung

Wenn Sie jedoch über eine Fehlerbehandlung nachdenken, MÜSSEN SIE Promise.all verwenden. Es ist nicht möglich, Fehler von asynchronen parallelen Aufgaben, die mit mehreren Wartezeiten ausgelöst wurden, korrekt zu behandeln. Im negativen Szenario enden Sie immer mit UnhandledPromiseRejectionWarningund PromiseRejectionHandledWarningobwohl Sie try / catch überall verwenden. Deshalb wurde Promise.all entwickelt. Natürlich könnte jemand sagen, dass wir diese Fehler mit process.on('unhandledRejection', err => {})und unterdrücken könnenprocess.on('rejectionHandled', err => {}) aber es ist keine gute Praxis. Ich habe im Internet viele Beispiele gefunden, bei denen die Fehlerbehandlung für zwei oder mehr unabhängige asynchrone parallele Aufgaben überhaupt nicht oder nur in falscher Weise berücksichtigt wird - nur mit try / catch und in der Hoffnung, dass Fehler abgefangen werden. Es ist fast unmöglich, eine gute Praxis zu finden. Deshalb schreibe ich diese Antwort.

Zusammenfassung

Verwenden Sie niemals mehrere Wartezeiten für zwei oder mehr unabhängige asynchrone parallele Aufgaben, da Sie Fehler nicht ernsthaft behandeln können. Verwenden Sie für diesen Anwendungsfall immer Promise.all (). Async / await ist kein Ersatz für Promises. Es ist nur eine hübsche Art, Versprechen zu verwenden ... asynchroner Code ist im Synchronisierungsstil geschrieben und wir können mehrere vermeidenthen Versprechen .

Einige Leute sagen, dass wir mit Promise.all () Aufgabenfehler nicht separat behandeln können, sondern nur Fehler aus dem ersten abgelehnten Versprechen (ja, einige Anwendungsfälle erfordern möglicherweise eine separate Behandlung, z. B. für die Protokollierung). Es ist kein Problem - siehe "Addition" unten.

Beispiele

Betrachten Sie diese asynchrone Aufgabe ...

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

Wenn Sie Aufgaben in einem positiven Szenario ausführen, gibt es keinen Unterschied zwischen Promise.all und Multiple Wait. Beide Beispiele enden Task 1 succeed! Task 2 succeed!nach 5 Sekunden.

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

Wenn die erste Aufgabe im positiven Szenario 10 Sekunden und die zweite Aufgabe im negativen Szenario 5 Sekunden dauert, gibt es Unterschiede bei den ausgegebenen Fehlern.

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

Wir sollten hier bereits bemerken, dass wir etwas falsch machen, wenn wir mehrere Wartezeiten parallel verwenden. Um Fehler zu vermeiden, sollten wir natürlich damit umgehen! Lass es uns versuchen...


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

Wie Sie sehen können, müssen wir nur einen Fang zur runFunktion hinzufügen, und der Code mit der Fanglogik befindet sich im Rückruf ( asynchroner Stil ), um Fehler erfolgreich zu behandeln . Wir brauchen keine Fehler innerhalb der runFunktion zu behandeln, da die asynchrone Funktion automatisch ausgeführt wird. Das Versprechen der Zurückweisung der taskFunktion führt zur Zurückweisung der runFunktion. Um Rückrufe zu vermeiden, können wir den Synchronisierungsstil (async / await + try / catch) verwenden. try { await run(); } catch(err) { }In diesem Beispiel ist dies jedoch nicht möglich, da wir ihn nicht awaitim Hauptthread verwenden können. Er kann nur in der asynchronen Funktion verwendet werden (logisch, weil niemand dies möchte Hauptgewinde blockieren). So testen Sie, ob die Behandlung im Synchronisierungsstil , können wir aufrufenrun von einer anderen asynchronen Funktion aus funktioniert, oder verwenden Sie IIFE (Sofort aufgerufener Funktionsausdruck) : (async function() { try { await run(); } catch(err) { console.log('Caught error', err); }; })();.

Dies ist nur eine korrekte Methode, um zwei oder mehr asynchrone parallele Aufgaben auszuführen und Fehler zu behandeln. Sie sollten Beispiele unten vermeiden.


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

Wir können versuchen, Code auf verschiedene Arten zu verarbeiten ...

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 

... nichts wurde abgefangen, weil es Synchronisationscode verarbeitet, aber runasynchron ist

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? Wir sehen erstens, dass der Fehler für Aufgabe 2 nicht behandelt wurde und später abgefangen wurde. Irreführend und immer noch voller Fehler in der Konsole. Auf diese Weise nicht verwendbar.

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

... das gleiche wie oben. Benutzer @Qwerty fragte in seiner gelöschten Antwort nach diesem seltsamen Verhalten, das anscheinend abgefangen wird, aber es gibt auch nicht behandelte Fehler. Wir fangen einen Fehler ab, weil run () online mit dem Schlüsselwort await abgelehnt wird und beim Aufruf von run () mit try / catch abgefangen werden kann. Wir erhalten auch einen nicht behandelten Fehler, weil wir die asynchrone Taskfunktion synchron aufrufen (ohne das Schlüsselwort wait) und diese Task außerhalb der Funktion run () ausgeführt wird und auch außerhalb fehlschlägt. Es ist ähnlich, wenn wir beim Aufrufen einer Synchronisierungsfunktion, deren Codeteil in setTimeout ausgeführt wird, nicht in der Lage sind, Fehler durch try / catch zu behandeln 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)

... "nur" zwei Fehler (dritter fehlt) aber nichts abgefangen.


Ergänzung (Behandeln Sie Aufgabenfehler separat und auch First-Fail-Fehler)

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!

... beachten Sie, dass ich in diesem Beispiel für beide Aufgaben negativeScenario = true verwendet habe, um besser zu demonstrieren, was passiert ( throw errwird verwendet, um den endgültigen Fehler auszulösen).

mikep
quelle
14
Diese Antwort ist besser als die akzeptierte Antwort, da die aktuell akzeptierte Antwort das sehr wichtige Thema der Fehlerbehandlung verfehlt
chrishiestand
7

Sie können es selbst überprüfen.

In dieser Geige habe ich einen Test durchgeführt, um die Blockierungsnatur von zu demonstrieren await, im Gegensatz dazu Promise.allwerden alle Versprechen gestartet und während einer wartet, wird es mit den anderen weitergehen.

zpr
quelle
6
Eigentlich spricht Ihre Geige seine Frage nicht an. Es gibt einen Unterschied zwischen dem Anrufen t1 = task1(); t2 = task2()und dem anschließenden Verwenden awaitfür beide result1 = await t1; result2 = await t2;wie in seiner Frage, im Gegensatz zu dem, was Sie testen, das awaitbeim ursprünglichen Anruf verwendet wird result1 = await task1(); result2 = await task2();. Der Code in seiner Frage startet alle Versprechen auf einmal. Der Unterschied besteht, wie die Antwort zeigt, darin, dass Fehler auf dem Promise.allWeg schneller gemeldet werden.
BryanGrezeszak
Ihre Antwort ist nicht zum Thema wie @BryanGrezeszak kommentiert. Sie sollten es lieber löschen, um irreführende Benutzer zu vermeiden.
Mikep
7

Bei Verwendung von Promise.all()Laufanforderungen wird im Allgemeinen "asynchron" parallel ausgeführt. Die Verwendung awaitkann parallel ausgeführt werden ODER eine "Synchronisierungs" -Blockierung sein.

Die folgenden Funktionen test1 und test2 zeigen, wie awaitasynchron oder synchron ausgeführt werden kann.

Test3 zeigt, Promise.all()dass dies asynchron ist.

jsfiddle with timed results - Öffnen Sie die Browserkonsole, um die Testergebnisse anzuzeigen

Synchronisierungsverhalten . Läuft NICHT parallel, dauert ~ 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
};

Asynchrones Verhalten. Läuft parallel, dauert ~ 600ms :

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
}

Asynchrones Verhalten. Läuft parallel, dauert ~ 600ms :

const test3 = async () => {
  await Promise.all([
  Promise.delay(600), 
  Promise.delay(600), 
  Promise.delay(600)]); //runs all delays simultaneously
};

TLDR; Wenn Sie es verwenden Promise.all(), wird es auch "schnell fehlschlagen" - stoppen Sie die Ausführung zum Zeitpunkt des ersten Ausfalls einer der enthaltenen Funktionen.

GavinBelson
quelle
1
Wo kann ich in den Ausschnitten 1 und 2 ausführlich erklären, was unter der Haube passiert? Ich bin so überrascht, dass diese eine andere Art zu laufen haben, da ich erwartet hatte, dass das Verhalten das gleiche ist.
Gregordy
2
@ Gregory ja es ist überraschend. Ich habe diese Antwort gepostet, um Codierer zu speichern, die neu sind, um einige Kopfschmerzen zu asynchronisieren. Es geht darum, wann JS das Warten auswertet. Deshalb ist es wichtig, wie Sie Variablen zuweisen. Ausführliche asynchrone Lesung: blog.bitsrc.io/…
GavinBelson
0

Im Falle von Warten auf Promise.all ([task1 (), task2 ()]); "task1 ()" und "task2 ()" werden parallel ausgeführt und warten, bis beide Versprechen erfüllt sind (entweder gelöst oder abgelehnt). Im Falle von

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

t2 wird erst ausgeführt, nachdem t1 die Ausführung beendet hat (wurde aufgelöst oder abgelehnt). Sowohl t1 als auch t2 laufen nicht parallel.

Waleed Naveed
quelle