Une différence entre await Promise.all () et multiple await?


Réponses:


209

Remarque :

Cette réponse ne couvre que les différences de synchronisation entre les awaitséries et Promise.all. Assurez-vous de lire la réponse complète de @ mikep qui couvre également les différences les plus importantes dans la gestion des erreurs .


Pour les besoins de cette réponse, j'utiliserai quelques exemples de méthodes:

  • res(ms) est une fonction qui prend un entier de millisecondes et renvoie une promesse qui se résout au bout de plusieurs millisecondes.
  • rej(ms) est une fonction qui prend un entier de millisecondes et renvoie une promesse qui rejette après ce nombre de millisecondes.

L'appel reslance le chronomètre. Utiliser Promise.allpour attendre une poignée de retards résoudra une fois tous les retards terminés, mais rappelez-vous qu'ils s'exécutent en même temps:

Exemple 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

Cela signifie que Promise.all cela résoudra avec les données des promesses internes après 3 secondes.

Mais, Promise.alla un comportement «échec rapide» :

Exemple # 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

Si vous utilisez à la async-awaitplace, vous devrez attendre que chaque promesse se résout séquentiellement, ce qui peut ne pas être aussi efficace:

Exemple # 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
Donc, fondamentalement, la différence est juste la fonction «échec rapide» de Promise.all?
Matthieu

4
@mclzc Dans l'exemple n ° 3, l'exécution ultérieure du code est interrompue jusqu'à ce que delay1 se résout. C'est même dans le texte "Si vous utilisez async-await à la place, vous devrez attendre que chaque promesse se résout séquentiellement"
haggis

1
@Qback, il existe un extrait de code en direct qui illustre le comportement. Pensez à l'exécuter et à relire le code. Vous n'êtes pas la première personne à mal comprendre comment se comporte la séquence de promesses. L'erreur que vous avez commise dans votre démo est que vous ne commencez pas vos promesses en même temps.
zzzzBov

1
@zzzzBov Vous avez raison. Vous le démarrez en même temps. Désolé, je suis venu à cette question pour une autre raison et j'ai oublié cela.
Qback

2
« ce n'est peut-être pas aussi efficace » - et plus important encore, provoquer des unhandledrejectionerreurs. Vous ne voudrez jamais l'utiliser. Veuillez ajouter ceci à votre réponse.
Bergi

87

Première différence - échouer rapidement

Je suis d'accord avec la réponse de @ zzzzBov mais l'avantage "échouer rapidement" de Promise.all n'est pas seulement la seule différence. Certains utilisateurs dans les commentaires demandent pourquoi utiliser Promise.all alors qu'il n'est plus rapide que dans un scénario négatif (lorsqu'une tâche échoue). Et je demande pourquoi pas? Si j'ai deux tâches parallèles asynchrones indépendantes et que la première est résolue dans un temps très long, mais la seconde est rejetée dans un délai très court, pourquoi laisser l'utilisateur attendre le message d'erreur "très long temps" au lieu de "très court temps"? Dans les applications réelles, nous devons envisager un scénario négatif. Mais OK - dans cette première différence, vous pouvez décider quelle alternative utiliser Promise.all par rapport à plusieurs attendent.

Deuxième différence - gestion des erreurs

Mais lorsque vous envisagez de gérer les erreurs, VOUS DEVEZ utiliser Promise.all. Il n'est pas possible de gérer correctement les erreurs des tâches parallèles asynchrones déclenchées avec plusieurs wait. Dans un scénario négatif, vous finirez toujours par UnhandledPromiseRejectionWarninget PromiseRejectionHandledWarningbien que vous utilisiez try / catch n'importe où. C'est pourquoi Promise.all a été conçu. Bien sûr, quelqu'un pourrait dire que nous pouvons supprimer les erreurs en utilisant process.on('unhandledRejection', err => {})etprocess.on('rejectionHandled', err => {}) mais ce n'est pas une bonne pratique. J'ai trouvé de nombreux exemples sur Internet qui ne prennent pas du tout en compte la gestion des erreurs pour deux ou plusieurs tâches parallèles asynchrones indépendantes ou qui ne le considèrent pas mais de la mauvaise manière - en utilisant simplement try / catch et en espérant qu'il détectera les erreurs. Il est presque impossible de trouver de bonnes pratiques. C'est pourquoi j'écris cette réponse.

Résumé

N'utilisez jamais d'attente multiple pour deux ou plusieurs tâches parallèles asynchrones indépendantes car vous ne serez pas en mesure de gérer les erreurs sérieusement. Utilisez toujours Promise.all () pour ce cas d'utilisation. Async / await ne remplace pas les promesses. C'est juste une jolie manière d'utiliser les promesses ... le code async est écrit dans le style de synchronisation et nous pouvons éviter plusieursthen promesses.

Certaines personnes disent qu'en utilisant Promise.all (), nous ne pouvons pas gérer les erreurs de tâches séparément mais seulement l'erreur de la première promesse rejetée (oui, certains cas d'utilisation peuvent nécessiter une gestion séparée, par exemple pour la journalisation). Ce n'est pas un problème - voir l'en-tête "Ajout" ci-dessous.

Exemples

Considérez cette tâche asynchrone ...

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

Lorsque vous exécutez des tâches dans un scénario positif, il n'y a aucune différence entre Promise.all et multiple await. Les deux exemples se terminent Task 1 succeed! Task 2 succeed!après 5 secondes.

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

Lorsque la première tâche prend 10 secondes dans un scénario positif et que la tâche secondes prend 5 secondes dans un scénario négatif, il existe des différences dans les erreurs émises.

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

Nous devrions déjà remarquer ici que nous faisons quelque chose de mal lors de l'utilisation de plusieurs wait en parallèle. Bien sûr, pour éviter les erreurs, nous devons le gérer! Essayons...


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

Comme vous pouvez le voir pour gérer correctement l'erreur, nous devons ajouter une seule capture à la runfonction et le code avec la logique de capture est en rappel ( style asynchrone ). Nous n'avons pas besoin de gérer les erreurs à l'intérieur de la runfonction car la fonction asynchrone le fait automatiquement - la promesse de rejet de la taskfonction entraîne le rejet de la runfonction. Pour éviter le rappel, nous pouvons utiliser le style de synchronisation (async / await + try / catch) try { await run(); } catch(err) { }mais dans cet exemple, ce n'est pas possible car nous ne pouvons pas l'utiliser awaitdans le thread principal - il ne peut être utilisé que dans la fonction async (c'est logique car personne ne veut bloquer le thread principal). Pour tester si la gestion fonctionne dans le style de synchronisation, nous pouvons appelerrunfonction d' une autre fonction asynchrone ou utilisation Ia vie (Expression immédiatement Appelé Function): (async function() { try { await run(); } catch(err) { console.log('Caught error', err); }; })();.

Ce n'est qu'une façon correcte d'exécuter deux ou plusieurs tâches parallèles asynchrones et de gérer les erreurs. Vous devriez éviter les exemples ci-dessous.


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

Nous pouvons essayer de gérer le code de plusieurs manières ...

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 

... rien n'a été attrapé car il gère le code de synchronisation mais runest asynchrone

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? Nous voyons tout d'abord que l'erreur de la tâche 2 n'a pas été gérée et plus tard qu'elle a été interceptée. Trompeur et toujours plein d'erreurs dans la console. Inutilisable de cette façon.

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

... le même que ci-dessus. L'utilisateur @Qwerty, dans sa réponse supprimée, a posé des questions sur ce comportement étrange qui semble être attrapé, mais il y a aussi des erreurs non gérées. Nous détectons l'erreur car run () est rejeté en ligne avec le mot-clé await et peut être détecté en utilisant try / catch lors de l'appel de run (). Nous obtenons également une erreur non gérée car nous appelons la fonction de tâche asynchrone de manière synchrone (sans mot-clé await) et cette tâche s'exécute en dehors de la fonction run () et échoue également à l'extérieur. Il est similaire quand nous ne sommes pas en mesure de gérer l' erreur par try / catch lorsque vous appelez une fonction de synchronisation qui partie de code se exécute dans 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)

... "seulement" deux erreurs (la troisième est manquante) mais rien n'a été détecté.


Ajout (gérer les erreurs de tâche séparément et également l'erreur de premier échec)

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!

... notez que dans cet exemple, j'ai utilisé negativeScenario = true pour les deux tâches pour une meilleure démonstration de ce qui se passe ( throw errest utilisé pour déclencher l'erreur finale)


14
cette réponse est meilleure que la réponse acceptée car la réponse actuellement acceptée manque le sujet très important de la gestion des erreurs
chrishiestand

8

Généralement, l'utilisation Promise.all()exécute des requêtes "asynchrones" en parallèle. L'utilisation awaitpeut s'exécuter en parallèle OU être un blocage de «synchronisation».

Les fonctions test1 et test2 ci-dessous montrent comment awaitexécuter async ou sync.

test3 montre Promise.all()que c'est asynchrone.

jsfiddle avec des résultats chronométrés - ouvrez la console du navigateur pour voir les résultats des tests

Comportement de synchronisation . Ne fonctionne PAS en parallèle, prend ~ 1800 ms :

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

Comportement asynchrone . Fonctionne en parallèle, prend ~ 600 ms :

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
}

Comportement asynchrone . Fonctionne en parallèle, prend ~ 600 ms :

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

TLDR; Si vous l'utilisez, Promise.all()il "échouera rapidement" - arrêtez de fonctionner au moment du premier échec de l'une des fonctions incluses.


1
Où puis-je obtenir une explication détaillée de ce qui se passe sous le capot dans les extraits 1 et 2? Je suis tellement surpris que ceux-ci aient une manière différente de fonctionner car je m'attendais à ce que les comportements soient les mêmes.
Gregordy

2
@Gregordy oui c'est surprenant. J'ai posté cette réponse pour sauver les nouveaux codeurs pour asynchroniser certains maux de tête. Tout dépend du moment où JS évalue l'attente, c'est pourquoi la façon dont vous attribuez des variables est importante. Lecture Async approfondie: blog.bitsrc.io
...

7

Vous pouvez vérifier par vous-même.

Dans ce violon , j'ai fait un test pour démontrer la nature bloquante de await, par opposition à Promise.alllaquelle déclenchera toutes les promesses et pendant que l'une attend, elle continuera avec les autres.


6
En fait, votre violon ne répond pas à sa question. Il y a une différence entre l' appel t1 = task1(); t2 = task2()et puis en utilisant awaitensuite pour les deux result1 = await t1; result2 = await t2;comme dans sa question, par opposition à ce que vous testez qui utilise awaitl'appel original comme result1 = await task1(); result2 = await task2();. Le code dans sa question lance toutes les promesses à la fois. La différence, comme le montre la réponse, est que les échecs seront signalés plus rapidement avec le Promise.allchemin.
BryanGrezeszak

Votre réponse est hors sujet, comme l'a commenté @BryanGrezeszak. Vous devriez plutôt le supprimer pour éviter de tromper les utilisateurs.
mikep le

0

En cas d' attente de Promise.all ([task1 (), task2 ()]); "task1 ()" et "task2 ()" fonctionneront en parallèle et attendront que les deux promesses soient complétées (résolues ou rejetées). Alors qu'en cas de

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

t2 ne fonctionnera qu'après la fin de l'exécution de t1 (résolu ou rejeté). Les deux t1 et t2 ne fonctionneront pas en parallèle.

En utilisant notre site, vous reconnaissez avoir lu et compris notre politique liée aux cookies et notre politique de confidentialité.
Licensed under cc by-sa 3.0 with attribution required.