Une comparaison entre async / await et puis / catch
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 seulement après la précédente l'un a été exécuté. 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 pour 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 promesses: puis
catch
et enfin
. Mais un an plus tard, dans ES7, le langage a ajouté une autre approche et deux nouveaux mots-clés: async
et wait
.
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 – puis / catch
ou async / await
– est la meilleure? À 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 commencerons par récapituler les principales caractéristiques de chaque syntaxe, avant de passer à notre exemple de scénario.
puis
catch
Et enfin
puis
et catch
et enfin
sont des méthodes de l'objet Promise, et elles sont enchaînés les uns après les autres. Chacun prend une fonction de rappel comme argument et renvoie une promesse.
Par exemple, instancions une simple promesse:
const salut = nouvelle promesse ((résoudre, rejeter) => {
résoudre ("Bonjour!");
});
En utilisant puis
catch
et enfin
nous pourrions effectuer une série d'actions selon que la promesse est résolue ( puis
) ou rejetée ( catch
) – alors que enfin
nous permet d'exécuter du code une fois la promesse réglée, qu'elle ait été résolue ou rejetée:
salut
.then ((valeur) => {
console.log ("La promesse est résolue!", valeur);
})
.catch ((erreur) => {
console.error ("La promesse est rejetée!", erreur);
})
.finalement (() => {
console.log (
"La promesse est réglée, ce qui signifie qu'elle a été résolue ou rejetée."
);
});
Pour les besoins de cet article, il suffit d'utiliser puis
. L'enchaînement de plusieurs méthodes puis
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 puis
pourrait ressembler à ceci:
fetch (url)
.then ((réponse) => réponse.json ())
.then ((données) => {
revenir {
données: données,
statut: response.status,
};
})
.then ((res) => {
console.log (res.data, res.status);
});
async
And await
En revanche, async
et await
sont des mots clés qui rendent le code synchrone asynchrone. Nous utilisons async
lors de la définition d'une fonction pour signifier qu'elle renvoie une promesse. Remarquez comment le placement du mot-clé async
dépend de l'utilisation de fonctions régulières ou de fonctions fléchées:
fonction async doSomethingAsynchronous () {
// logique
}
const doSomethingAsynchronous = async () => {
// logique
};
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 salutation
ci-dessus, nous pourrions écrire:
fonction async doSomethingAsynchronous () {
valeur const = attendre le message d'accueil;
}
Nous pouvons alors utiliser notre variable value
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
instruction, comme ceci:
fonction async doSomethingAsynchronous () {
essayez {
valeur const = attendre le message d'accueil;
console.log ("La promesse est résolue!", valeur);
} catch (e) {
console.error ("La promesse est rejetée!", erreur);
} enfin {
console.log (
"La promesse est réglée, ce qui signifie qu'elle a été résolue ou rejetée."
);
}
}
Enfin, lorsque vous renvoyez une promesse dans une fonction async
vous n’avez pas besoin d’utiliser wait
. La syntaxe suivante est donc acceptable.
fonction async getGreeting () {
retourner le message d'accueil;
}
Cependant, il y a une exception à cette règle: vous devez écrire return wait
si vous cherchez à gérer le rejet de la promesse dans un try ... catch
block.
fonction asynchrone getGreeting () {
essayez {
retour attendre salutation;
} 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 pourrait être préférable à l'autre jusqu'à ce que nous sautions dans un exemple.
Le problème
Imaginons que nous ayons besoin d'effectuer une opération sur un grand ensemble de données 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 - renvoie tous les auteurs de la base de données
// getBooks - renvoie tous les livres de la base de données
// getBio - renvoie la biographie d'un auteur spécifique
Nos objets ressemblent à ceci:
// Auteur: {id: "3b4ab205", nom: "Frank Herbert Jr.", bioId: "1138089a"}
// Livre: {id: "e31f7b5e", title: "Dune", authorId: "3b4ab205"}
// Bio: {id: "1138089a", description: "Franklin Herbert Jr. était un auteur américain de science-fiction ..."}
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 () {
renvoyer auteurs.filtre (
({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 tous les auteurs
RECHERCHE tous les livres
FILTER auteurs avec plus de 10 livres
POUR chaque auteur filtré
FETCH la biographie de l'auteur
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 nous pourrions coder ces étapes en utilisant puis
:
getAuthors (). Then ((auteurs) =>
getBooks ()
.then ((livres) => {
const prolificAuthorIds = filterProlificAuthors (auteurs, livres);
return Promise.all (prolificAuthorIds.map ((id) => getBio (id)));
})
.then ((bios) => {
// Faites quelque chose avec le bios
})
);
Ce code fait le travail, mais il y a des imbrications en cours qui peuvent le rendre difficile à comprendre en un coup d'œil. Le deuxième puis
est imbriqué dans le premier puis
tandis que le troisième puis
est parallèle au second.
Notre code pourrait devenir un peu plus lisible si nous avons utilisé puis
pour renvoyer même le code synchrone? Nous pourrions donner à filterProlificAuthors
sa propre méthode puis
comme ci-dessous:
getAuthors (). Then ((auteurs) =>
getBooks ()
.then ((livres) => filterProlificAuthors (auteurs, livres))
.then ((ids) => Promise.all (ids.map ((id) => getBio (id))))
.then ((bios) => {
// Faites quelque chose avec le bios
})
);
Cette version présente l'avantage que chaque méthode puis
tient sur une seule ligne, mais elle ne nous évite pas d'avoir plusieurs niveaux d'imbrication.
Qu'en est-il de l'utilisation de async
] et attendent
? Notre premier passage à une solution pourrait ressembler à ceci:
fonction async getBios () {
auteurs const = attendre getAuthors ();
livres const = attendre getBooks ();
const prolificAuthorIds = filterProlificAuthors (auteurs, livres);
const bios = attendre Promise.all (prolificAuthorIds.map ((id) => getBio (id)));
// Faites quelque chose avec le 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
deviendront plus évidents à mesure que nos exigences changent.
Partie 2
Introduisons une nouvelle exigence. Cette fois, une fois que nous avons notre tableau bios
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
:
fonction async getBios () {
auteurs const = attendre getAuthors ();
livres const = attendre getBooks ();
const prolificAuthorIds = filterProlificAuthors (auteurs, livres);
const bios = attendre Promise.all (prolificAuthorIds.map ((id) => getBio (id)));
résultat const = {
biographie,
totalAuthors: auteurs.longueur,
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 objet résultat
à la fin.
Avec puis
ce n’est pas si simple. Dans notre solution puis
de la partie 1, les variables livres
et bios
ne sont jamais dans la même portée. Alors que nous pourrions introduire une variable globale books
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 faire?
Une option serait d'introduire un troisième niveau d'imbrication:
getAuthors (). Then ((auteurs) =>
getBooks (). then ((livres) => {
const prolificAuthorIds = filterProlificAuthors (auteurs, livres);
renvoie Promise.all (prolificAuthorIds.map ((id) => getBio (id))). then (
(bios) => {
résultat const = {
biographie,
totalAuthors: auteurs.longueur,
totalBooks: books.length,
};
}
);
})
);
Alternativement, nous pourrions utiliser la syntaxe de déstructuration de tableau pour aider à faire passer livres
à travers la chaîne à chaque étape:
getAuthors (). Then ((auteurs) =>
getBooks ()
.then ((livres) => [books, filterProlificAuthors(authors, books)])
.then (([books, ids]) =>
Promise.all ([books, ...ids.map((id) => getBio(id))])
)
.then (([books, bios]) => {
résultat const = {
biographie,
totalAuthors: auteurs.longueur,
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 et où.
Partie 3
En guise d'optimisation finale, nous pouvons améliorer les performances de notre solution et la nettoyer un peu en utilisant Promise. tous
pour aller chercher les auteurs et les livres en même temps. Cela aide à nettoyer un peu notre solution puis
:
Promise.all ([getAuthors(), getBooks()]). Then (([authors, books]) => {
const prolificAuthorIds = filterProlificAuthors (auteurs, livres);
return Promise.all (prolificAuthorIds.map ((id) => getBio (id))). then ((bios) => {
résultat const = {
biographie,
totalAuthors: auteurs.longueur,
totalBooks: books.length,
};
});
});
C'est peut-être la meilleure solution puis
du groupe. Cela supprime le besoin de plusieurs niveaux d'imbrication et le code s'exécute plus rapidement.
Néanmoins, async / await
reste plus simple:
fonction async getBios () {
const [authors, books] = attendre Promise.all ([getAuthors(), getBooks()]);
const prolificAuthorIds = filterProlificAuthors (auteurs, livres);
const bios = attendre Promise.all (prolificAuthorIds.map ((id) => getBio (id)));
résultat const = {
biographie,
totalAuthors: auteurs.longueur,
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 crochets!
Conclusion
Souvent, l'utilisation de méthodes chaînées puis
peut nécessiter 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 puis
avait des compromis différents pour la lisibilité. En revanche, async / await
se prêtait à une solution plus lisible qui devait très peu changer lorsque les exigences de notre problème étaient modifiées.
Dans les applications réelles, les exigences de notre code asynchrone seront souvent plus complexe que le scénario présenté ici. Alors que async / await
nous fournit une base facile à comprendre pour écrire une logique plus délicate, l'ajout de nombreuses méthodes puis
peut facilement nous forcer plus loin sur la voie de l'enfer du rappel – avec de nombreux crochets
Pour cette raison – si vous avez le choix – choisissez async / await
sur puis / catch
.

Source link