Catégories
Astuces et Design

Une comparaison entre async / await et puis / catch – Smashing Magazine

A propos de l'auteur

Bret Cameron est un développeur et écrivain basé à Londres. C'est un ingénieur full-stack à la startup insurtech YuLife, et il est passionné par tout…
Plus à propos
Bret

En JavaScript, il existe deux manières principales de gérer le code asynchrone: then/catch (ES6) et async/await (ES7). Ces syntaxes nous donnent les mêmes fonctionnalités sous-jacentes, mais elles affectent la lisibilité et la portée de différentes manières. Dans cet article, nous verrons comment une syntaxe se prête à un code maintenable, tandis que l'autre nous met sur la voie de l'enfer des rappels!

JavaScript exécute le code ligne par ligne, passant à la ligne de code suivante uniquement après l'exécution de la précédente. Mais exécuter un code comme celui-ci ne peut nous mener que très loin. Parfois, nous devons effectuer des tâches qui prennent un temps long ou imprévisible: récupérer des données ou déclencher des effets secondaires via une API, par exemple.

Plutôt que de laisser ces tâches bloquer le thread principal de JavaScript, le langage nous permet d'exécuter certaines tâches en parallèle. ES6 a vu l'introduction de l'objet Promise ainsi que de nouvelles méthodes pour gérer l'exécution de ces Promises: then, catch, et finally. Mais un an plus tard, dans ES7, le langage a ajouté une autre approche et deux nouveaux mots-clés: async et await.

Cet article n’explique pas le JavaScript asynchrone; il y a beaucoup de bonnes ressources disponibles pour cela. Au lieu de cela, il aborde un sujet moins couvert: quelle syntaxe – then/catch ou async/await – est mieux? À mon avis, à moins qu'une bibliothèque ou une base de code héritée ne vous oblige à utiliser then/catch, le meilleur choix pour la lisibilité et la maintenabilité est async/await. Pour le démontrer, nous utiliserons les deux syntaxes pour résoudre le même problème. En modifiant légèrement les exigences, il devrait devenir clair quelle approche est la plus facile à modifier et à maintenir.

Nous allons commencer par récapituler les principales caractéristiques de chaque syntaxe, avant de passer à notre exemple de scénario.

then, catch Et finally

then et catch et finally sont des méthodes de l'objet Promise, et elles sont enchaînées les unes après les autres. Chacun prend une fonction de rappel comme argument et renvoie une promesse.

Par exemple, instancions une simple promesse:

const greeting = new Promise((resolve, reject) => {
  resolve("Hello!");
});

En utilisant then, catch et finally, nous pourrions effectuer une série d'actions selon que la promesse est résolue (then) ou rejetée (catch) – tandis que finally nous permet d'exécuter du code une fois la promesse réglée, qu'elle ait été résolue ou rejetée:

greeting
  .then((value) => {
    console.log("The Promise is resolved!", value);
  })
  .catch((error) => {
    console.error("The Promise is rejected!", error);
  })
  .finally(() => {
    console.log(
      "The Promise is settled, meaning it has been resolved or rejected."
    );
  });

Pour les besoins de cet article, nous devons uniquement utiliser then. Chaînage multiple then méthodes nous permet d'effectuer des opérations successives sur une promesse résolue. Par exemple, un modèle typique pour récupérer des données avec then pourrait ressembler à quelque chose comme ceci:

fetch(url)
  .then((response) => response.json())
  .then((data) => {
    return {
      data: data,
      status: response.status,
    };
  })
  .then((res) => {
    console.log(res.data, res.status);
  });

async Et await

Par contre, async et await sont des mots-clés qui rendent le code d'asynchrone asynchrone. Nous utilisons async lors de la définition d'une fonction pour signifier qu'elle renvoie une promesse. Remarquez comment le placement du async Le mot clé dépend de l'utilisation de fonctions normales ou de fonctions fléchées:

async function doSomethingAsynchronous() {
  // logic
}

const doSomethingAsynchronous = async () => {
  // logic
};

await, quant à lui, est utilisé avant une promesse. Il met en pause l'exécution d'une fonction asynchrone jusqu'à ce que la promesse soit résolue. Par exemple, pour attendre notre greeting ci-dessus, on pourrait écrire:

async function doSomethingAsynchronous() {
  const value = await greeting;
}

Nous pouvons alors utiliser notre value variable comme si elle faisait partie d'un code synchrone normal.

En ce qui concerne la gestion des erreurs, nous pouvons envelopper n'importe quel code asynchrone dans un try...catch...finally déclaration, comme ceci:

async function doSomethingAsynchronous() {
  try {
    const value = await greeting;
    console.log("The Promise is resolved!", value);
  } catch (e) {
    console.error("The Promise is rejected!", error);
  } finally {
    console.log(
      "The Promise is settled, meaning it has been resolved or rejected."
    );
  }
}

Enfin, lors du retour d'une promesse dans un async fonction, vous n’avez pas besoin d’utiliser await. La syntaxe suivante est donc acceptable.

async function getGreeting() {
  return greeting;
}

Cependant, il existe une exception à cette règle: vous devez écrire return await si vous souhaitez gérer le rejet de la promesse dans un try...catch bloquer.

async function getGreeting() {
  try {
    return await greeting;
  } catch (e) {
    console.error(e);
  }
}

L’utilisation d’exemples abstraits peut nous aider à comprendre chaque syntaxe, mais il est difficile de comprendre pourquoi l’une est préférable à l’autre tant que nous n’avons pas lancé un exemple.

Le problème

Imaginons que nous devions effectuer une opération sur un ensemble de données volumineux pour une librairie. Notre tâche est de trouver tous les auteurs qui ont écrit plus de 10 livres dans notre ensemble de données et de renvoyer leur biographie. Nous avons accès à une bibliothèque avec trois méthodes asynchrones:

// getAuthors - returns all the authors in the database
// getBooks - returns all the books in the database
// getBio - returns the bio of a specific author

Nos objets ressemblent à ceci:

// Author: { id: "3b4ab205", name: "Frank Herbert Jr.", bioId: "1138089a" }
// Book: { id: "e31f7b5e", title: "Dune", authorId: "3b4ab205" }
// Bio: { id: "1138089a", description: "Franklin Herbert Jr. was an American science-fiction author..." }

Enfin, nous aurons besoin d'une fonction d'assistance, filterProlificAuthors, qui prend tous les articles et tous les livres comme arguments, et renvoie les identifiants de ces auteurs avec plus de 10 livres:

function filterProlificAuthors() {
  return authors.filter(
    ({ id }) => books.filter(({ authorId }) => authorId === id).length > 10
  );
}

La solution

Partie 1

Pour résoudre ce problème, nous devons récupérer tous les auteurs et tous les livres, filtrer nos résultats en fonction de nos critères donnés, puis obtenir la biographie de tous les auteurs qui correspondent à ces critères. En pseudo-code, notre solution pourrait ressembler à ceci:

FETCH all authors
FETCH all books
FILTER authors with more than 10 books
FOR each filtered author
  FETCH the author’s bio

Chaque fois que nous voyons FETCH ci-dessus, nous devons effectuer une tâche asynchrone. Alors, comment pourrions-nous transformer cela en JavaScript? Voyons d'abord comment coder ces étapes à l'aide de then:

getAuthors().then((authors) =>
  getBooks()
    .then((books) => {
      const prolificAuthorIds = filterProlificAuthors(authors, books);
      return Promise.all(prolificAuthorIds.map((id) => getBio(id)));
    })
    .then((bios) => {
      // Do something with the bios
    })
);

Ce code fait le travail, mais il y a une imbrication en cours qui peut le rendre difficile à comprendre en un coup d'œil. La deuxième then est imbriqué dans le premier then, tandis que le troisième then est parallèle à la seconde.

Notre code pourrait devenir un peu plus lisible si nous utilisions then retourner même le code synchrone? Nous pourrions donner filterProlificAuthors sa propre then méthode, comme ci-dessous:

getAuthors().then((authors) =>
  getBooks()
    .then((books) => filterProlificAuthors(authors, books))
    .then((ids) => Promise.all(ids.map((id) => getBio(id))))
    .then((bios) => {
      // Do something with the bios
    })
);

Cette version a l'avantage que chacun then La méthode tient sur une seule ligne, mais elle ne nous évite pas d'avoir plusieurs niveaux d'imbrication.

Qu'en est-il de l'utilisation async et await? Notre premier passage à une solution pourrait ressembler à ceci:

async function getBios() {
  const authors = await getAuthors();
  const books = await getBooks();
  const prolificAuthorIds = filterProlificAuthors(authors, books);
  const bios = await Promise.all(prolificAuthorIds.map((id) => getBio(id)));
  // Do something with the bios
}

Pour moi, cette solution me paraît déjà plus simple. Il n'implique aucune imbrication et peut être facilement exprimé en seulement quatre lignes – toutes au même niveau d'indentation. Cependant, les avantages de async/await deviendra plus évidente à mesure que nos exigences changent.

Partie 2

Introduisons une nouvelle exigence. Cette fois, une fois que nous avons notre bios array, nous voulons créer un objet contenant bios, le nombre total d'auteurs et le nombre total de livres.

Cette fois, nous allons commencer par async/await:

async function getBios() {
  const authors = await getAuthors();
  const books = await getBooks();
  const prolificAuthorIds = filterProlificAuthors(authors, books);
  const bios = await Promise.all(prolificAuthorIds.map((id) => getBio(id)));
  const result = {
    bios,
    totalAuthors: authors.length,
    totalBooks: books.length,
  };
}

Facile! Nous n'avons rien à faire sur notre code existant, car toutes les variables dont nous avons besoin sont déjà dans la portée. Nous pouvons simplement définir notre result objet à la fin.

Avec then, ce n’est pas si simple. Dans notre then solution de la partie 1, la books et bios les variables ne sont jamais dans la même portée. Pendant que nous pourrait introduire un global books variable, cela polluerait l'espace de noms global avec quelque chose dont nous n'avons besoin que dans notre code asynchrone. Il vaudrait mieux reformater notre code. Alors, comment pourrions-nous le faire?

Une option serait d'introduire un troisième niveau d'imbrication:

getAuthors().then((authors) =>
  getBooks().then((books) => {
    const prolificAuthorIds = filterProlificAuthors(authors, books);
    return Promise.all(prolificAuthorIds.map((id) => getBio(id))).then(
      (bios) => {
        const result = {
          bios,
          totalAuthors: authors.length,
          totalBooks: books.length,
        };
      }
    );
  })
);

Alternativement, nous pourrions utiliser la syntaxe de déstructuration de tableau pour aider à passer books à travers la chaîne à chaque étape:

getAuthors().then((authors) =>
  getBooks()
    .then((books) => (books, filterProlificAuthors(authors, books)))
    .then(((books, ids)) =>
      Promise.all((books, ...ids.map((id) => getBio(id))))
    )
    .then(((books, bios)) => {
      const result = {
        bios,
        totalAuthors: authors.length,
        totalBooks: books.length,
      };
    })
);

Pour moi, aucune de ces solutions n'est particulièrement lisible. Il est difficile de déterminer – en un coup d’œil – quelles variables sont accessibles à quel endroit.

Partie 3

En guise d'optimisation finale, nous pouvons améliorer les performances de notre solution et la nettoyer un peu en utilisant Promise.all pour aller chercher les auteurs et les livres en même temps. Cela aide à nettoyer notre then solution un peu:

Promise.all((getAuthors(), getBooks())).then(((authors, books)) => {
  const prolificAuthorIds = filterProlificAuthors(authors, books);
  return Promise.all(prolificAuthorIds.map((id) => getBio(id))).then((bios) => {
    const result = {
      bios,
      totalAuthors: authors.length,
      totalBooks: books.length,
    };
  });
});

C'est peut-être le meilleur then solution du bouquet. Cela supprime le besoin de plusieurs niveaux d'imbrication et le code s'exécute plus rapidement.

Cependant, async/await reste plus simple:

async function getBios() {
  const (authors, books) = await Promise.all((getAuthors(), getBooks()));
  const prolificAuthorIds = filterProlificAuthors(authors, books);
  const bios = await Promise.all(prolificAuthorIds.map((id) => getBio(id)));
  const result = {
    bios,
    totalAuthors: authors.length,
    totalBooks: books.length,
  };
}

Il n'y a pas d'imbrication, un seul niveau d'indentation et beaucoup moins de risques de confusion basée sur les parenthèses!

Conclusion

Souvent, en utilisant des then Les méthodes peuvent exiger des modifications fastidieuses, en particulier lorsque nous voulons nous assurer que certaines variables sont dans la portée. Même pour un scénario simple comme celui dont nous avons discuté, il n'y avait pas de meilleure solution évidente: chacune des cinq solutions utilisant then avait différents compromis pour la lisibilité. Par contre, async/await se prêtait à une solution plus lisible qui devait très peu changer lorsque les exigences de notre problème étaient ajustées.

Dans les applications réelles, les exigences de notre code asynchrone seront souvent plus complexes que le scénario présenté ici. Tandis que async/await nous fournit une base facile à comprendre pour écrire une logique plus délicate, en ajoutant de nombreux then Les méthodes peuvent facilement nous forcer plus loin sur la voie de l'enfer du rappel – avec de nombreux crochets et niveaux d'indentation qui ne permettent pas de savoir où se termine un bloc et où commence le suivant.

Pour cette raison – si vous avez le choix – choisissez async/await plus de then/catch.

Éditorial fracassant(sh, ra, yk, il)

Laisser un commentaire

Votre adresse de messagerie ne sera pas publiée. Les champs obligatoires sont indiqués avec *