
L'auteur de l'article analyse Async / Await en JavaScript à l'aide d'exemples. En général, Async / Await est un moyen pratique d'écrire du code asynchrone. Avant cette opportunité, un code similaire a été écrit à l'aide de rappels et de promesses. L'auteur de l'article original révèle les avantages d'Async / Await en examinant divers exemples.
Nous vous rappelons: pour tous les lecteurs de «Habr» - une remise de 10 000 roubles lors de l'inscription à un cours Skillbox en utilisant le code promo «Habr».
Skillbox recommande: Le cours de formation en ligne pour les développeurs Java .
Rappel
Le rappel est une fonction dont l'appel est retardé indéfiniment. Auparavant, les rappels étaient utilisés dans les parties du code où le résultat ne pouvait pas être obtenu immédiatement.
Voici un exemple de lecture asynchrone d'un fichier sur Node.js:
fs.readFile(__filename, 'utf-8', (err, data) => { if (err) { throw err; } console.log(data); });
Des problèmes surviennent lorsque vous devez effectuer plusieurs opérations asynchrones à la fois. Imaginons ce scénario: une demande est faite à la base de données utilisateurs Arfat, vous devez lire son champ profile_img_url et télécharger une image depuis le serveur someserver.com.
Après le téléchargement, convertissez l'image dans un autre format, par exemple, de PNG en JPEG. Si la conversion a réussi, un e-mail est envoyé au courrier de l'utilisateur. De plus, des informations sur l'événement sont entrées dans le fichier transformations.log avec la date.

Il convient de prêter attention à l'imposition de rappels et d'un grand nombre}) dans la dernière partie du code. Cela s'appelle un enfer de rappel ou une pyramide de malheur.
Les inconvénients de cette méthode sont évidents:
- Ce code est difficile à lire.
- Il est également difficile d'en traiter les erreurs, ce qui entraîne souvent une détérioration de la qualité du code.
Afin de résoudre ce problème, des promesses ont été ajoutées à JavaScript. Ils vous permettent de remplacer l'imbrication profonde des rappels par le mot .then.

Le point positif des promesses était qu'avec elles le code est beaucoup mieux lu, de haut en bas, et non de gauche à droite. Néanmoins, les promesses ont aussi leurs problèmes:
- Besoin d'ajouter une grande quantité de .puis.
- Au lieu de try / catch, .catch est utilisé pour gérer toutes les erreurs.
- Travailler avec plusieurs promesses dans un cycle est loin d'être toujours pratique; dans certains cas, elles compliquent le code.
Voici une tâche qui montrera la signification du dernier paragraphe.
Supposons qu'il existe une boucle for qui imprime une séquence de nombres de 0 à 10 avec un intervalle aléatoire (0 - n secondes). En utilisant des promesses, vous devez modifier ce cycle afin que les nombres soient affichés dans la séquence de 0 à 10. Donc, si la sortie zéro prend 6 secondes et les unités prennent 2 secondes, zéro doit d'abord être sorti, puis le compte à rebours de sortie de l'unité commence.
Et bien sûr, pour résoudre ce problème, nous n'utilisons pas Async / Await ou .sort. Un exemple de solution se trouve à la fin.
Fonctions asynchrones
L'ajout de fonctions asynchrones à ES2017 (ES8) a simplifié la tâche de travailler avec des promesses. Je note que les fonctions asynchrones fonctionnent en plus des promesses. Ces fonctions ne représentent pas des concepts qualitativement différents. Les fonctions asynchrones ont été conçues comme une alternative au code qui utilise des promesses.
Async / Await permet d'organiser le travail avec du code asynchrone dans un style synchrone.
Ainsi, la connaissance des promesses facilite la compréhension des principes d'Async / Await.
SyntaxeDans une situation typique, il se compose de deux mots clés: async et wait. Le premier mot rend la fonction asynchrone. Ces fonctions permettent d'attendre. Dans tous les autres cas, l'utilisation de cette fonction entraînera une erreur.
Async est inséré au tout début de la déclaration de fonction, et dans le cas de la fonction flèche, entre le signe "=" et les crochets.
Ces fonctions peuvent être placées dans un objet en tant que méthodes ou utilisées dans une déclaration de classe.
NB! Il convient de rappeler que les constructeurs de classes et les getters / setters ne peuvent pas être asynchrones.
Sémantique et règles d'exécutionLes fonctions asynchrones sont fondamentalement similaires aux fonctions JS standard, mais il existe des exceptions.
Ainsi, les fonctions asynchrones renvoient toujours des promesses:
async function fn() { return 'hello'; } fn().then(console.log)
En particulier, fn renvoie la chaîne hello. Eh bien, comme il s'agit d'une fonction asynchrone, la valeur de la chaîne est enveloppée dans une promesse utilisant le constructeur.
Voici une conception alternative sans Async:
function fn() { return Promise.resolve('hello'); } fn().then(console.log);
Dans ce cas, le retour de la promesse se fait "manuellement". Une fonction asynchrone s'enveloppe toujours dans une nouvelle promesse.
Dans le cas où la valeur de retour est une primitive, la fonction asynchrone renvoie une valeur en l'enveloppant dans une promesse. Dans le cas où la valeur de retour est l'objet de la promesse, sa solution est retournée dans la nouvelle promesse.
const p = Promise.resolve('hello') p instanceof Promise;
Mais que se passe-t-il si une erreur se produit à l'intérieur de la fonction asynchrone?
async function foo() { throw Error('bar'); } foo().catch(console.log);
S'il n'est pas traité, foo () renverra une promesse avec un redject. Dans cette situation, au lieu de Promise.resolve, Promise.reject renverra contenant une erreur.
Les fonctions asynchrones en sortie donnent toujours des promesses, indépendamment de ce qui est retourné.
Les fonctions asynchrones sont suspendues à chaque attente.
Attendre affecte les expressions. Ainsi, si l'expression est une promesse, la fonction asynchrone est suspendue jusqu'à l'exécution de la promesse. Dans le cas où l'expression n'est pas une promesse, elle est convertie en promesse via Promise.resolve puis arrêtée.
Voici une description du fonctionnement de la fonction fn.
- Après l'avoir appelé, la première ligne est convertie de const a = attendre 9; dans const a = attendre Promise.resolve (9);.
- Après avoir utilisé Await, l'exécution de la fonction est suspendue jusqu'à ce qu'elle reçoive sa valeur (dans la situation actuelle, elle est 9).
- delayAndGetRandom (1000) suspend l'exécution de la fonction fn jusqu'à ce qu'elle se termine (après 1 seconde). En fait, cela arrête la fonction fn pendant 1 seconde.
- delayAndGetRandom (1000) via la résolution renvoie une valeur aléatoire, qui est ensuite affectée à la variable b.
- Eh bien, le cas de la variable c est similaire au cas de la variable a. Après cela, tout s'arrête pendant une seconde, mais maintenant delayAndGetRandom (1000) ne renvoie rien, car cela n'est pas obligatoire.
- En conséquence, les valeurs sont calculées par la formule a + b * c. Le résultat est encapsulé dans une promesse à l'aide de Promise.resolve et renvoyé par la fonction.
Ces pauses peuvent ressembler à des générateurs dans ES6, mais il y a des
raisons à cela .
Nous résolvons le problème
Eh bien, regardons maintenant la solution au problème mentionné ci-dessus.

La fonction finishMyTask utilise Await pour attendre les résultats d'opérations telles que queryDatabase, sendEmail, logTaskInFile et autres. Si nous comparons cette décision avec l'endroit où les promesses ont été utilisées, les similitudes deviendront apparentes. Néanmoins, la version avec Async / Await simplifie considérablement toutes les difficultés syntaxiques. Dans ce cas, il n'y a pas beaucoup de rappels et de chaînes comme .then / .catch.
Voici une solution avec la sortie des nombres, il y a deux options.
const wait = (i, ms) => new Promise(resolve => setTimeout(() => resolve(i), ms));
Et voici une solution utilisant des fonctions asynchrones.
async function printNumbersUsingAsync() { for (let i = 0; i < 10; i++) { await wait(i, Math.random() * 1000); console.log(i); } }
Gestion des erreursLes erreurs non traitées sont enveloppées dans des promesses rejetées. Cependant, dans les fonctions asynchrones, vous pouvez utiliser la construction try / catch pour effectuer une gestion des erreurs synchrone.
async function canRejectOrReturn() {
canRejectOrReturn () est une fonction asynchrone qui réussit («nombre parfait») ou échoue avec une erreur («Désolé, nombre trop grand»).
async function foo() { try { await canRejectOrReturn(); } catch (e) { return 'error caught'; } }
Étant donné que canRejectOrReturn devrait s'exécuter dans l'exemple ci-dessus, sa propre terminaison infructueuse entraînera l'exécution du bloc catch. Par conséquent, la fonction foo se terminera soit avec undefined (lorsque rien n'est retourné dans le bloc try) soit avec une erreur interceptée. En conséquence, cette fonction n'échouera pas, car try / catch gérera la fonction foo elle-même.
Voici un autre exemple:
async function foo() { try { return canRejectOrReturn(); } catch (e) { return 'error caught'; } }
Il convient de prêter attention au fait que dans l'exemple de foo, canRejectOrReturn est retourné. Foo dans ce cas se termine par un nombre parfait ou renvoie une erreur Error ("Désolé, nombre trop grand"). Le bloc catch ne sera jamais exécuté.
Le problème est que foo renvoie la promesse passée de canRejectOrReturn. Par conséquent, la solution de la fonction foo devient la solution de canRejectOrReturn. Dans ce cas, le code ne comprendra que deux lignes:
try { const promise = canRejectOrReturn(); return promise; }
Mais que se passe-t-il si vous utilisez attendre et revenir ensemble:
async function foo() { try { return await canRejectOrReturn(); } catch (e) { return 'error caught'; } }
Dans le code ci-dessus, foo réussit avec un nombre parfait et une erreur interceptée. Il n'y aura pas d'échecs. Mais foo se terminera par canRejectOrReturn, et non par undefined. Assurons-nous de cela en supprimant la ligne de retour en attente canRejectOrReturn ():
try { const value = await canRejectOrReturn(); return value; }
Erreurs et pièges courants
Dans certains cas, l'utilisation d'Async / Await peut entraîner des erreurs.
Oublié attendreCela se produit assez souvent - avant la promesse, le mot-clé wait est oublié:
async function foo() { try { canRejectOrReturn(); } catch (e) { return 'caught'; } }
Dans le code, comme vous pouvez le voir, il n'y a ni attente ni retour. Par conséquent, foo sort toujours avec undefined sans délai de 1 seconde. Mais la promesse sera tenue. S'il donne une erreur ou un redject, alors UnhandledPromiseRejectionWarning sera appelé.
Fonctions asynchrones dans les rappelsLes fonctions asynchrones sont souvent utilisées dans .map ou .filter comme rappels. Un exemple est la fonction fetchPublicReposCount (username), qui renvoie le nombre de référentiels ouverts sur GitHub. Disons qu'il y a trois utilisateurs dont nous avons besoin de mesures. Voici le code de cette tâche:
const url = 'https://api.github.com/users';
Nous avons besoin de comptes ArfatSalman, octocat, norvig. Dans ce cas, exécutez:
const users = [ 'ArfatSalman', 'octocat', 'norvig' ]; const counts = users.map(async username => { const count = await fetchPublicReposCount(username); return count; });
Vous devez faire attention à Attendre dans le rappel .map. Ici, compte est un tableau de promesses, eh bien .map est un rappel anonyme pour chaque utilisateur spécifié.
Utilisation excessivement cohérente de l'attentePrenez le code suivant comme exemple:
async function fetchAllCounts(users) { const counts = []; for (let i = 0; i < users.length; i++) { const username = users[i]; const count = await fetchPublicReposCount(username); counts.push(count); } return counts; }
Ici, le numéro de dépôt est placé dans la variable de comptage, puis ce nombre est ajouté au tableau des comptages. Le problème avec le code est que jusqu'à ce que les premières données utilisateur arrivent du serveur, tous les utilisateurs suivants seront en mode veille. Ainsi, en un seul instant, un seul utilisateur est traité.
Si, par exemple, il faut environ 300 ms pour traiter un utilisateur, alors pour tous les utilisateurs, c'est déjà une seconde, le temps passé linéairement dépend du nombre d'utilisateurs. Mais puisque l'obtention du nombre de dépôts ne dépend pas les uns des autres, les processus peuvent être parallélisés. Cela nécessite de travailler avec .map et Promise.all:
async function fetchAllCounts(users) { const promises = users.map(async username => { const count = await fetchPublicReposCount(username); return count; }); return Promise.all(promises); }
Promise.all à l'entrée reçoit un tableau de promesses avec le retour de la promesse. La dernière après la fin de toutes les promesses dans le tableau ou au premier redject est terminée. Il peut arriver que tous ne démarrent pas en même temps - afin d'assurer un lancement simultané, vous pouvez utiliser p-map.
Conclusion
Les fonctionnalités asynchrones deviennent de plus en plus importantes pour le développement. Eh bien, pour une utilisation adaptative des fonctions asynchrones, il vaut la peine d'utiliser des
itérateurs asynchrones. Le développeur JavaScript doit être bien familiarisé avec cela.
Skillbox recommande: