L'auteur de l'article examine, à travers des exemples, Async/Await en JavaScript. En général, Async/Await est un moyen pratique d'écrire du code asynchrone. Avant cette possibilité, ce type de code était écrit en utilisant des callbacks et des promesses. L'auteur de l'article original dévoile les avantages d'Async/Await en explorant divers exemples.
Rappelons-le : pour tous les lecteurs de « Habr » — une réduction de 10 000 roubles lors de l'inscription à tout cours Skillbox avec le code promo « Habr ».
Skillbox recommande : Cours en ligne éducatif .
Callback
Un callback est une fonction dont l'appel est retardé jusqu'à un moment indéfini. Auparavant, les callbacks é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 de fichier sur Node.js :
fs.readFile(__filename, 'utf-8', (err, data) => {
if (err) {
throw err;
}
console.log(data);
}); Des problèmes surviennent lorsque plusieurs opérations asynchrones doivent être exécutées simultanément. Imaginons le scénario suivant : une requête est effectuée dans la base de données pour l'utilisateur Arfat, il est nécessaire de lire son champ profile_img_url et de télécharger l'image depuis le serveur someserver.com.
Après le téléchargement, nous convertissons l'image dans un autre format, par exemple de PNG à JPEG. Si la conversion réussit, un e-mail est envoyé à l'utilisateur. Ensuite, les informations concernant l'événement sont enregistrées dans le fichier transformations.log avec la date.
Il convient de prêter attention à l'imbrication des callbacks et au grand nombre de }) dans la partie finale du code. Cela s'appelle Callback Hell ou Pyramid of Doom.
Les inconvénients de cette méthode sont évidents :
- Ce code est difficile à lire.
- Il est également difficile de gérer les erreurs, ce qui entraîne souvent une dégradation de la qualité du code.
Pour résoudre ce problème, des promesses ont été ajoutées à JavaScript. Elles permettent de remplacer la profonde imbrication des callbacks par le mot .then.
Un aspect positif des promesses est qu'elles rendent le code beaucoup plus lisible, de haut en bas, plutôt que de gauche à droite. Néanmoins, les promesses ont aussi leurs propres problèmes :
- Il faut ajouter un grand nombre de .then.
- Au lieu de try/catch, on utilise .catch pour gérer toutes les erreurs.
- Travailler avec plusieurs promesses dans une même boucle n'est pas toujours pratique, et dans certains cas, cela complexifie le code.
Voici une tâche qui mettra en évidence la valeur du dernier point.
Supposons qu'il y ait une boucle for affichant une séquence de nombres de 0 à 10 avec un intervalle aléatoire (0 à n secondes). En utilisant des promesses, il faut modifier cette boucle de sorte que les nombres soient affichés dans l'ordre de 0 à 10. Ainsi, si l'affichage du zéro prend 6 secondes et celui de un 2 secondes, le zéro doit d'abord être affiché, puis commencera le compte à rebours pour le un.
Et bien sûr, pour résoudre ce problème, nous n'utilisons pas Async/Await ni .sort. Un exemple de solution se trouve à la fin.
Fonctions async
L'ajout des fonctions async dans ES2017 (ES8) a simplifié la tâche de travail avec des promesses. Je souligne que les fonctions async fonctionnent "au-dessus" des promesses. Ces fonctions ne représentent pas des concepts fondamentalement différents. Les fonctions async ont été conçues comme une alternative au code utilisant des promesses.
Async/Await permet d'organiser le travail avec du code asynchrone dans un style synchrone.
Ainsi, connaître les promesses facilite la compréhension des principes d'Async/Await.
Syntaxe
Dans une situation normale, elle se compose de deux mots clés : async et await. Le premier mot transforme la fonction en asynchrone. Dans ces fonctions, l'utilisation de await est autorisée. Dans tous les autres cas, l'utilisation de cette fonction entraînera une erreur.
// With function declaration
async function myFn() {
// await ...
}
// With arrow function
const myFn = async () => {
// await ...
}
function myFn() {
// await fn(); (Syntax Error since no async)
}
Async est inséré au tout début de la déclaration de la fonction, et dans le cas d'une fonction fléchée — entre le signe "=" et les parenthèses.
Ces fonctions peuvent être placées dans un objet en tant que méthodes ou utilisées dans la déclaration d'une classe.
// As an object's method
const obj = {
async getName() {
return fetch('https://www.example.com');
}
}
// In a class
class Obj {
async getResource() {
return fetch('https://www.example.com');
}
}NB ! Il convient de rappeler que les constructeurs de classe et les getters/setters ne peuvent pas être asynchrones.
Sémantique et règles d'exécution
Les fonctions async sont en principe similaires aux fonctions JS standard, mais il y a des exceptions.
Ainsi, les fonctions async renvoient toujours des promesses :
async function fn() {
return 'hello';
}
fn().then(console.log)
// helloEn particulier, fn renvoie la chaîne hello. Comme il s'agit d'une fonction asynchrone, la valeur de la chaîne est enveloppée dans une promesse à l'aide du constructeur.
Voici une construction alternative sans Async :
function fn() {
return Promise.resolve('hello');
}
fn().then(console.log);
// helloDans ce cas, le retour de promesse est effectué "manuellement". Une fonction asynchrone est toujours enveloppée dans une nouvelle promesse.
Si la valeur retournée est un primitive, la fonction async renvoie cette valeur en l'enveloppant dans une promesse. Si la valeur retournée est déjà un objet promesse, sa résolution est retournée dans une nouvelle promesse.
const p = Promise.resolve('hello')
p instanceof Promise;
// true
Promise.resolve(p) === p;
// true
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);Si elle n'est pas traitée, foo() renverra une promesse avec un rejet. Dans ce cas, au lieu de Promise.resolve, Promise.reject sera retournée, contenant l'erreur.
Les fonctions async renvoient toujours une promesse, quelle que soit la valeur retournée.
Les fonctions asynchrones sont suspendues à chaque await.
Await affecte les expressions. Ainsi, si l'expression est une promesse, la fonction async est suspendue jusqu'à l'exécution de celle-ci. Si l'expression n'est pas une promesse, elle est convertie en promesse via Promise.resolve et se termine ensuite.
// utility function to cause delay
// and get random value
const delayAndGetRandom = (ms) => {
return new Promise(resolve => setTimeout(
() => {
const val = Math.trunc(Math.random() * 100);
resolve(val);
}, ms
));
};
async function fn() {
const a = await 9;
const b = await delayAndGetRandom(1000);
const c = await 5;
await delayAndGetRandom(1000);
return a + b * c;
}
// Execute fn
fn().then(console.log);Voici une description du fonctionnement de la fonction fn.
- Après son appel, la première ligne est convertie de const a = await 9; en const a = await Promise.resolve(9);.
- Après l'utilisation de Await, l'exécution de la fonction est suspendue jusqu'à ce que a obtienne sa valeur (dans ce cas, c'est 9).
- delayAndGetRandom(1000) suspend l'exécution de la fonction fn jusqu'à ce qu'elle se termine (après 1 seconde). Cela revient à arrêter la fonction fn pendant 1 seconde.
- delayAndGetRandom(1000) renvoie une valeur aléatoire via resolve, qui est ensuite assignée à la variable b.
- Et le cas de la variable c est similaire à celui de la variable a. Après cela, tout s'arrête pendant une seconde, mais maintenant delayAndGetRandom(1000) ne retourne rien, car ce n'est pas nécessaire.
- Finalement, les valeurs sont calculées selon la formule a + b * c. Le résultat est enveloppé dans une promesse à l'aide de Promise.resolve et retourné par la fonction.
Ces pauses peuvent rappeler les générateurs en ES6, mais il y a .
Résolvons le problème
Maintenant, examinons 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 d'autres. Si l'on compare cette solution avec celle utilisant des promesses, la similitude devient évidente. Néanmoins, la version avec Async/Await simplifie considérablement toutes les complexités syntaxiques. Dans ce cas, il n'y a pas beaucoup de callbacks et de chaînes comme .then/.catch.
Voici une solution avec l'affichage de nombres, il y a deux variantes ici.
const wait = (i, ms) => new Promise(resolve => setTimeout(() => resolve(i), ms));
// Implementation One (Using for-loop)
const printNumbers = () => new Promise((resolve) => {
let pr = Promise.resolve(0);
for (let i = 1; i <= 10; i += 1) {
pr = pr.then((val) => {
console.log(val);
return wait(i, Math.random() * 1000);
});
}
resolve(pr);
});
// Implementation Two (Using Recursion)
const printNumbersRecursive = () => {
return Promise.resolve(0).then(function processNextPromise(i) {
if (i === 10) {
return undefined;
}
return wait(i, Math.random() * 1000).then((val) => {
console.log(val);
return processNextPromise(i + 1);
});
});
};Voici une solution utilisant des fonctions async.
async function printNumbersUsingAsync() {
for (let i = 0; i < 10; i++) {
await wait(i, Math.random() * 1000);
console.log(i);
}
}Gestion des erreurs
Les erreurs non traitées sont enveloppées dans une promesse rejetée. Cependant, dans les fonctions async, on peut utiliser la structure try/catch pour effectuer une gestion des erreurs de manière synchrone.
async function canRejectOrReturn() {
// attendre une seconde
await new Promise(res => setTimeout(res, 1000));
// Rejeter avec ~50% de probabilité
if (Math.random() > 0.5) {
throw new Error('Désolé, le nombre est trop grand.')
}
return 'nombre parfait';
}canRejectOrReturn() est une fonction asynchrone qui s'exécute soit avec succès ('nombre parfait'), soit échoue avec une erreur ('Désolé, le nombre est trop grand').
async function foo() {
try {
await canRejectOrReturn();
} catch (e) {
return 'erreur capturée';
}
}Comme dans l'exemple ci-dessus, on attend l'exécution de canRejectOrReturn, donc un échec entraînera l'exécution du bloc catch. Par conséquent, la fonction foo se terminera soit avec undefined (lorsque rien n'est renvoyé dans le bloc try), soit avec erreur capturée. Ainsi, cette fonction n'aura pas d'échec, car try/catch gère l'exécution même de la fonction foo.
Voici un autre exemple :
async function foo() {
try {
return canRejectOrReturn();
} catch (e) {
return 'erreur capturée';
}
}Il convient de noter que dans l'exemple de foo, canRejectOrReturn est renvoyé. Foo, dans ce cas, se termine soit par un nombre parfait, soit avec une erreur Error ('Désolé, le nombre est trop grand'). Le bloc catch ne sera jamais exécuté.
Le problème est que foo renvoie une promesse, passée de canRejectOrReturn. Ainsi, la résolution de la fonction foo devient celle de canRejectOrReturn. Dans ce cas, le code se composera de seulement deux lignes :
try {
const promise = canRejectOrReturn();
return promise;
}Voici ce qui se passe si vous utilisez à la fois await et return :
async function foo() {
try {
return await canRejectOrReturn();
} catch (e) {
return 'erreur capturée';
}
}Dans le code ci-dessus, foo se termine avec succès, que ce soit avec un nombre parfait ou avec une erreur interceptée. Il n'y aura pas d'échecs ici. Mais foo se terminera avec canRejectOrReturn, et non avec undefined. Assurons-nous de cela en supprimant la ligne return await canRejectOrReturn():
try {
const value = await canRejectOrReturn();
return value;
}
// …Erreurs courantes et pièges
Dans certains cas, l'utilisation d'Async/Await peut entraîner des erreurs.
Await oublié
Cela arrive assez souvent : le mot clé await est oublié devant la promesse :
async function foo() {
try {
canRejectOrReturn();
} catch (e) {
return 'caught';
}
}Dans le code, comme vous pouvez le voir, il n'y a ni await ni return. Par conséquent, foo se termine toujours par undefined sans délai d'une seconde. Mais la promesse sera exécutée. Si elle lève une erreur ou est rejetée, dans ce cas, UnhandledPromiseRejectionWarning sera déclenché.
Fonctions asynchrones dans des rappels
Les fonctions asynchrones sont souvent utilisées dans .map ou .filter comme rappels. Prenons l'exemple de la fonction fetchPublicReposCount(username), qui retourne le nombre de dépôts publics sur GitHub. Supposons qu'il y ait trois utilisateurs dont nous avons besoin des statistiques. Voici le code pour cette tâche :
const url = 'https://api.github.com/users';
// Fonction utilitaire pour récupérer les comptes de dépôts
const fetchPublicReposCount = async (username) => {
const response = await fetch(`${url}/${username}`);
const json = await response.json();
return json['public_repos'];
}Nous avons besoin des comptes ArfatSalman, octocat, norvig. Dans ce cas, nous effectuons :
const users = [
'ArfatSalman',
'octocat',
'norvig'
];
const counts = users.map(async username => {
const count = await fetchPublicReposCount(username);
return count;
});Il convient de noter l'utilisation d'Await dans le rappel .map. Ici, counts est un tableau de promesses, et .map est un rappel anonyme pour chaque utilisateur spécifié.
Utilisation excessive d'await séquentielle
Prenons comme exemple le code suivant :
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, la variable count contient le nombre de dépôts, puis ce nombre est ajouté au tableau counts. Le problème avec le code est que, tant que les données du premier utilisateur ne sont pas reçues du serveur, tous les utilisateurs suivants restent en attente. Ainsi, à un moment donné, un seul utilisateur est traité.
Par exemple, si le traitement d'un utilisateur prend environ 300 ms, pour tous les utilisateurs, cela représente déjà une seconde, le temps consacré dépend linéairement du nombre d'utilisateurs. Cependant, comme l'obtention du nombre de dépôts ne dépend pas les uns des autres, les processus peuvent être parallélisés. Pour cela, il est nécessaire 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 prend en entrée un tableau de promesses et retourne une promesse. Celle-ci se termine après que toutes les promesses dans le tableau sont complètes ou au premier rejet. Il se peut que toutes ne s'exécutent pas simultanément ; pour assurer un lancement simultané, vous pouvez utiliser p-map.
Conclusion
Les fonctions Async deviennent de plus en plus importantes pour le développement. Pour utiliser les fonctions async de manière réactive, il vaut mieux se servir de . Un développeur JavaScript doit bien les comprendre.
Skillbox recommande :
- Cours pratique .
- Cours en ligne appliqué .
- Cours pratique de deux ans .
Source : habr.com
