Programmation asynchrone avec async et await¶
Si l’introduction des promesses a permis de formaliser le développement de code asynchrone, elle n’a pas permis de résoudre un problème inhérent à l’utilisation des fonctions callback : la difficulté de lecture et donc de maintenance d’un tel code.
Nous pouvons nous inspirer de l’exemple vu au chapitre précédent d’une promesse pour un appel ajax et créer une fonction pour réaliser des appels asynchrones vers un serveur pour récupérer un document JSON :
function getJson(url) {
return new Promise((resolve, reject) => {
let request = new XMLHttpRequest();
request.open("GET", url);
request.addEventListener("load", function(){
if(request.status === 200) {
resolve(JSON.parse(request.responseText));
} else {
reject("Erreur du serveur : " + request.status);
}
}, false);
request.addEventListener("error", function(){
reject("La requête ajax a échoué");
}, false);
request.send();
});
}
Nous pouvons appeler cette fonction :
1 2 3 4 5 6 7 8 | getJson("https://monserveur/data.json")
.then(json => {
document.getElementById("nom") = json.nom;
document.getElementById("prenom") = json.prenom;
})
.catch(msg => {
document.getElementById("message") = msg;
});
|
Le code ci-dessus est très simple… et pourtant pas si facile à lire que ça. Il faut garder à l’esprit que toutes ces arrow functions passées comme callback ne seront pas forcément appelées. Il serait plus intéressant de pouvoir écrire le même code séquentiellement car il devient plus facile à comprendre.
try {
const json = getJson("https://monserveur/data.json");
document.getElementById("nom") = json.nom;
document.getElementById("prenom") = json.prenom;
} catch(msg) {
document.getElementById("message") = msg;
}
C’est exactement pour cela que les mots-clés async
et await
ont été
introduits.
Fonction asynchrone¶
Une fonction peut être déclarée asynchrone grâce au mot clé async
. Une fonction
asynchrone est une fonction qui appelle une fonction qui retourne une promesse
ou une autre fonction asynchrone. Une fonction asynchrone peut appeler une fonction
qui retourne une promesse ou une fonction asynchrone en spécifiant le mot-clé
await
. Si nous reprenons l’exemple de la fonction getJson
, elle retourne
une promesse. Elle peut donc être appelée dans une fonction asynchrone à l’aide
du mot-clé await
:
async function maFonctionAsynchrone() {
const json = await getJson("https://monserveur/data.json");
// ...
}
Le mot-clé await
fonctionne de la manière suivante : il récupère la promesse
retournée par la fonction appelée et suspend l’exécution de la fonction asynchrone.
Lorsque la promesse est résolue, c’est-à-dire lorsque la fonction resolve
est
appelée par la promesse, alors l’objet passé en paramètre est retourné par
await
et le traitement de la fonction continue au point où il avait été suspendu.
Si la promesse échoue, c’est-à-dire si c’est la fonction reject
qui est appelée
par la promesse, alors la valeur passée en paramètre est jetée comme une exception.
Ainsi, grâce aux mots-clés async
et await
, nous pouvons écrire
séquentiellement des traitements qui sont en fait asynchrones. Attention, le mot-clé
await
ne peut être utilisé que dans le corps d’une fonction asynchrone.
Nous allons donc définir une fonction pour réécrire notre traitement :
async function miseAJour() {
try {
const json = await getJson("https://monserveur/data.json");
document.getElementById("nom") = json.nom;
document.getElementById("prenom") = json.prenom;
} catch(msg) {
document.getElementById("message") = msg;
}
}
miseAJour();
Note
Une fonction asynchrone peut elle-même retourner une valeur. Dans ce cas, la valeur retournée est une promesse. Si ce n’est pas le cas, alors une promesse est créée puis résolue avec la valeur initialement retournée.
Ainsi :
async function foo() {
return 1;
}
est équivalent à
async function foo() {
return Promise.resolve(1);
}
Ordre des appels dans un traitement asynchrone¶
La simplicité de lecture offerte par l’introduction des mots-clés async
et
await
est indéniablement importante. Cependant, il ne faut pas oublier que
les appels restent asynchrones et donc que l’ordre d’exécution n’est pas
nécessairement celui induit par la lecture du code. Pour nous en convaincre,
nous allons prendre un exemple simpliste. Imaginons que nous voulions coder
une méthode asynchrone pour réaliser l’addition de deux nombres… ce qui n’a
aucun intérêt à part de nous servir d’illustration.
function addition (a, b) {
return new Promise(resolve => {
resolve(a + b);
});
}
La fonction addition
retourne une promesse qui se résoudra par la somme des
paramètres.
Nous pouvons déclarer une fonction asynchrone qui appelle cette fonction avec
await
et qui écrit le résultat de l’appel dans la console :
async function calculer() {
console.log(await addition(2, 3))
}
Si nous appelons cette fonction :
calculer();
La valeur 5
s’affiche dans la console. Mais que se passe-t-il si nous
modifions légèrement notre programme de la façon suivante :
console.log("Avant l'appel à calculer");
calculer();
console.log("Après l'appel à calculer");
En exécutant ce code, nous obtenons le résultat suivant dans la console :
Avant l'appel à calculer
Après l'appel à calculer
5
Ce résultat peut être difficile à comprendre. On a l’impression que la fonction
calculer
n’a pas été appelée au moment attendu. En fait, il faut se souvenir
que la fonction calculer
est asynchrone et que le modèle d’exécution de
l’interpréteur JavaScript est limité à un flux de traitement (mono threaded).
Reprenons le programme : on commence par afficher le message « Avant l’appel à
calculer », puis on appelle la fonction calculer
. Cette fonction est une
fonction asynchrone. Elle appelle la fonction addition
avec le mot-clé
await
. Cette dernière fonction retourne une promesse qui sera appelée de
manière asynchrone lorsque l’interpréteur JavaScript aura fini d’exécuter le
flux de traitement courant car il ne peut traiter qu’un flux à la fois. Le
traitement de la fonction calculer
est donc suspendu et on passe à l’étape
suivante du programme qui consiste à afficher le message « Après l’appel à
calculer ». Comme le programme est maintenant terminé, le flux de traitement
est disponible pour exécuter la promesse. Cette dernière est résolue en
fournissant le résultat de l’addition et le traitement de la fonction calculer
peut enfin reprendre et afficher le résultat de l’addition.
On voit que, même si le programme nous semble écrit de manière séquentielle, l’ordre d’exécution est bien différent. Il faut surtout se rappeler que pour qu’une fonction asynchrone s’exécute, il faut que le traitement courant se termine le plus vite possible pour permettre à l’interpréteur de passer au traitement asynchrone.
La boucle for await¶
Il existe une variante de l’itération avec for await ... of
. Il est ainsi
possible de parcourir des tableaux, des chaînes de caractères, des collections
de nœuds DOM… de manière asynchrone. Comme pour le mot-clé await
, la
structure for await ... of
ne peut être utilisée que dans une fonction asynchrone.
async function afficherNombre(...values) {
for await (let v of values) {
console.log(v);
}
}
Si nous appelons deux fois de suite cette fonction :
afficherNombre(1, 2, 3, 4, 5, 6, 7, 8, 9, 10);
afficherNombre(101, 102, 103, 104, 105, 106, 107, 108, 109, 110);
Alors nous verrons bien le comportement asynchrone puisque l’affichage entremêlera les traitements des deux tableaux de valeurs :
1
101
2
102
3
103
4
104
5
105
6
106
7
107
8
108
9
109
10
110
Si vous voulez créer votre propre itérable pour l’utiliser avec la structure
for await ... of
, alors cet objet doit fournir une méthode @@asyncIterator
.
Cette méthode doit retourner un itérateur déclarant une méthode next
.
const premiersNombres = {
[Symbol.asyncIterator]() {
return {
i: 0,
next() {
if (this.i < 10) {
return {value: this.i++, done: false};
}
return {done: true};
}
}
}
}
async function afficherPremiersNombres() {
for await (let v of premiersNombres) {
console.log(v);
}
}
La structure for await ... of
accepte également les générateurs. Cela permet
d’appeler de manière asynchrone une fonction générative :
function* premiersNombres(max) {
for (var i = 0; i <= max; i++) {
yield i;
}
}
async function afficherPremiersNombres() {
for await (let v of premiersNombres(10)) {
console.log(v);
}
}