web-dev-qa-db-fra.com

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

Y a-t-il une différence entre:

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

et

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

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

et

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

Remarque :

Cette réponse ne couvre que les différences de synchronisation entre await en série et Promise.all. Assurez-vous de lire 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, je vais utiliser quelques exemples de méthodes:

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

L'appel de res démarre le chronomètre. Utiliser Promise.all pour attendre quelques retards sera résolu après que tous les retards soient 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
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()

Cela signifie que Promise.all sera résolu avec les données des promesses internes au bout de 3 secondes.

Mais, Promise.all a un comportement "échec rapide" :

Exemple n ° 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()

Si vous utilisez plutôt async-await, vous devrez attendre que chaque promesse soit résolue séquentiellement, ce qui risque de ne pas être aussi efficace:

Exemple n ° 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()
136
zzzzBov

Première différence - échouer vite

Je suis d’accord avec la réponse de @ zzzzBov mais je sais que "l’échec est rapide" de Promise.all n’est pas la seule différence. Dans les commentaires, certains utilisateurs demandent pourquoi utiliser Promise.all lorsque le scénario négatif n'est que plus rapide (en cas d'échec d'une tâche). Et je demande pourquoi pas? Si j'ai deux tâches parallèles asynchrones indépendantes et que la première est résolue très longtemps, mais que la seconde est rejetée très rapidement, pourquoi laisser l'utilisateur attendre le message d'erreur "très longtemps" au lieu de "très peu de 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 vs multiple wait.

Deuxième différence - traitement 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 de tâches parallèles asynchrones déclenchées avec plusieurs attentes. Dans un scénario négatif, vous finirez toujours par UnhandledPromiseRejectionWarning et PromiseRejectionHandledWarning bien 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 ces erreurs en utilisant process.on('unhandledRejection', err => {}) et process.on('rejectionHandled', err => {}), mais ce n'est pas une bonne pratique. J'ai trouvé de nombreux exemples sur Internet qui ne prenaient pas du tout en charge la gestion des erreurs pour deux tâches parallèles asynchrones indépendantes ou ne le considéraient pas, mais de manière erronée - 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.

Sommaire

Ne jamais utiliser plusieurs files d'attente pour plusieurs tâches parallèles asynchrones indépendantes, car vous ne pourrez pas gérer les erreurs sérieusement. Utilisez toujours Promise.all () pour ce cas d'utilisation. Async/wait ne remplace pas Promesses. C’est juste une belle façon d’utiliser les promesses ... Le code async est écrit dans le style de synchronisation et nous pouvons éviter plusieurs then dans les promesses.

Certaines personnes disent qu'en utilisant Promise.all (), nous ne pouvons pas gérer les erreurs de tâches séparément, mais uniquement les erreurs provenant 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 la rubrique "Ajout" ci-dessous.

Exemples

Considérez cette tâche async ...

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 pas de différence entre Promise.all et plusieurs en attente. Les deux exemples se terminent par 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 en secondes prend 5 secondes dans un scénario négatif, des erreurs sont générées.

// 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 lorsque nous utilisons plusieurs files d'attente en parallèle. Bien sûr, pour éviter les erreurs, nous devons nous en occuper! 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 les erreurs, nous devons ajouter une seule capture à la fonction run et le code avec la logique de capture est en rappel ( style asynchrone ). Nous n'avons pas besoin des erreurs de traitement dans la fonction run, car cette fonction est automatique - le rejet de promesse de la fonction task entraîne le rejet de la fonction run. Pour éviter les rappels, nous pouvons utiliser le style de synchronisation (async/wait + try/catch) try { await run(); } catch(err) { } mais dans cet exemple, cela n'est pas possible car ne peut pas utiliser await dans le thread principal - il ne peut être utilisé que dans une fonction asynchrone (logique car personne ne veut bloquer le thread principal). Pour vérifier si la manipulation fonctionne dans le style de synchronisation , nous pouvons appeler la fonction run à partir d'une autre fonction asynchrone ou utiliser IIFE (expression de fonction immédiatement appelée): (async function() { try { await run(); } catch(err) { console.log('Caught error', err); }; })();.

Ceci est seulement une façon correcte d'exécuter deux tâches parallèles asynchrones ou plus 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 ci-dessus de plusieurs façons ...

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é intercepté car il gère le code de synchronisation mais run est 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 d’abord que l’erreur pour la tâche 2 n’a pas été traitée et que cela a été intercepté plus tard. Tromper 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)

... les mêmes que ci-dessus. Dans sa réponse supprimée, l'utilisateur @Qwerty a posé une question sur ce comportement étrange qui semble être attrapé, mais il y a aussi des erreurs non gérées. Nous interceptons une erreur car run () est rejeté en ligne avec le mot-clé wait et peut être intercepté à l'aide de try/catch lors de l'appel de run (). Nous obtenons également une erreur non gérée parce que nous appelons la fonction de tâche asynchrone de manière synchrone (sans mot-clé wait) et que cette tâche est exécutée en dehors de la fonction run () et échoue également en dehors. Il en est de même lorsque nous ne pouvons pas gérer les erreurs en essayant/interceptant lorsque vous appelez une fonction de synchronisation, quelle partie du code est exécutée 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 3ème est manquante) mais rien ne s'accroche.


Ajout (gérer les erreurs de tâche séparément et aussi l'erreur du 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 err est utilisé pour déclencher l'erreur finale)

31
mikep

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.all, qui lancera toutes les promesses et qui attendra. avec les autres.

5
zpr