Programmation asynchrone
Cours · JavaScript : rendre le Web interactif (école d'ingénieur), chapitre 9 · école d'ingénieur, informatique · le début du chapitre, la suite sur Adloun
Au chapitre 8, la Librairie du Campus a appris à réagir : un clic ouvre le menu, chaque frappe filtre le catalogue. Mais ses livres vivent encore dans un module JavaScript, livres.js. Sur un vrai site, ils sont sur un serveur, et les obtenir prend du temps : quelques dizaines de millisecondes sur une bonne connexion, plusieurs secondes dans un train. Que fait la page pendant ce temps ? Elle ne doit ni se figer, ni afficher un catalogue vide sans explication.
En Java, on aurait lancé un thread : un second fil d'exécution qui attend la réponse pendant que le premier continue. JavaScript n'a qu'un seul fil pour exécuter le code de la page. Il attend autrement : il confie l'attente au navigateur, et reçoit plus tard un message, « c'est arrivé ». Comprendre ce mécanisme, la boucle d'événements, explique l'ordre parfois surprenant des affichages ; les promesses et les mots async/await permettent ensuite d'écrire du code asynchrone presque comme du code ordinaire. C'est la condition pour dialoguer avec un serveur, au chapitre 10.
Tous les programmes du chapitre sont dans le dossier code/ch9 du livre ; on lance ceux qui se terminent par .mjs avec Node (commande node microtaches.mjs dans un terminal), et l'on ouvre les pages par le petit serveur du livre, présenté au chapitre 10.
Objectifs du chapitre
- Expliquer le modèle d'exécution de JavaScript : un seul fil, une pile d'appels, une boucle d'événements, une file de tâches et une file de microtâches.
- Prévoir l'ordre des affichages d'un programme qui mêle code synchrone, minuteurs et promesses.
- Utiliser
setTimeout,setIntervalet leurs annulations. - Créer, enchaîner et combiner des promesses (
then,catch,finally,Promise.allet ses cousines). - Écrire des fonctions
async, choisir entre attente en série et en parallèle, traiter les erreurs avectry/catch. - Éviter les pièges classiques :
awaitoublié,forEachavec une fonctionasync, promesse rejetée que personne ne rattrape.
9.1 Un seul fil d'exécution
Le moteur JavaScript d'une page exécute le code sur un seul fil (thread) : une seule instruction à la fois, jamais deux fonctions en même temps. Un code est synchrone quand chaque instruction attend la fin de la précédente.
Ce fil est partagé avec l'affichage de la page : tant qu'un calcul l'occupe, le navigateur ne redessine rien et ne traite aucun clic. Le programme suivant programme un minuteur de 100 millisecondes, puis occupe le fil pendant 500 millisecondes.
// Une boucle de calcul occupe le seul fil : le minuteur attend qu'elle finisse.
const debut = Date.now();
setTimeout(() => {
console.log(`minuteur exécuté après ${Date.now() - debut} ms`);
}, 100);
// Environ 500 ms de calcul ininterrompu.
while (Date.now() - debut < 500) {
// rien : on occupe le processeur
}
console.log(`calcul terminé après ${Date.now() - debut} ms`);
$ node un-seul-fil.mjs
calcul terminé après 500 ms
minuteur exécuté après 501 ms
Le minuteur devait se déclencher à 100 ms ; il s'exécute à 501 ms, juste après la boucle. Le délai d'un minuteur est un minimum, jamais une garantie : la fonction ne peut pas interrompre le code en cours.
Dans une page, l'effet se voit. La page gel/ a deux boutons : « Calculer pendant 2 s » lance une boucle de deux secondes, « Cliquez-moi » note l'heure de son clic. On clique sur le premier, puis deux fois sur le second pendant le calcul :

Pendant deux secondes, la page n'a pas réagi. Les deux clics n'ont pas été perdus : le navigateur les a mis en attente, et les a fait traiter dès que le fil s'est libéré, à 2004 ms.
Différence avec Java
En Java, une méthode longue n'empêche pas l'interface Swing de vivre si on la confie à un autre thread. En JavaScript, le code de la page n'a pas d'autre thread : un calcul de plus de quelques dizaines de millisecondes se sent (page qui « rame »), un calcul d'une seconde la gèle. La règle est donc de ne jamais attendre activement : on demande au navigateur de nous rappeler. (Pour un vrai calcul lourd, le navigateur offre des Web Workers, des fils séparés qui communiquent par messages ; ils sortent du cadre de ce livre.)
9.1.1 La pile d'appels
La pile d'appels (call stack) contient les fonctions en cours d'exécution : appeler une fonction empile un cadre, en revenir le dépile. Le code en bas de la pile a appelé celui qui est au-dessus.
Comme en Java, une erreur non rattrapée affiche cette pile, du haut (là où l'erreur s'est produite) vers le bas (le script principal) :
// La pile d'appels, lue dans la trace d'une erreur.
function total(panier) {
return panier.reduce((somme, ligne) => somme + prixLigne(ligne), 0);
}
function prixLigne(ligne) {
return ligne.livre.prix * ligne.quantite;
}
console.log(total([{ livre: { prix: 32 }, quantite: 2 }]));
console.log(total([{ quantite: 1 }]));
$ node pile.mjs
64
TypeError: Cannot read properties of undefined (reading 'prix')
at prixLigne (…/pile.mjs:7:22)
at …/pile.mjs:3:50
at Array.reduce (<anonymous>)
at total (…/pile.mjs:3:17)
at …/pile.mjs:11:13
(Node écrit aussi la ligne fautive du source, et le chemin complet du fichier : nous avons retiré la première, abrégé le second en « … », et omis les dernières lignes de la trace, internes à Node.) On lit : le script principal (ligne 11) a appelé total, qui a appelé reduce, qui a appelé la fonction fléchée de la ligne 3, qui a appelé prixLigne, où ligne.livre valait undefined.
9.2 Minuteurs et boucle d'événements
9.2.1 setTimeout et setInterval
setTimeout(f, ms)demande d'exécuterfune fois, dans au moinsmsmillisecondes ; elle renvoie un identifiant, queclearTimeout(id)utilise pour annuler.setInterval(f, ms)répèteftoutes lesmsmillisecondes, jusqu'àclearInterval(id).- Les deux fonctions reviennent immédiatement : elles ne font qu'enregistrer la demande.
console.log("1. début");
setTimeout(() => console.log("4. minuteur de 100 ms"), 100);
setTimeout(() => console.log("3. minuteur de 0 ms"), 0);
console.log("2. fin du script");
$ node ordre-minuteurs.mjs
1. début
2. fin du script
3. minuteur de 0 ms
4. minuteur de 100 ms
Même avec un délai nul, la fonction passe après la fin du script. Le compte à rebours suivant répète un affichage chaque seconde et s'arrête lui-même :
// Un compte à rebours : setInterval répète, clearInterval arrête.
let reste = 3;
const id = setInterval(() => {
console.log(reste);
reste--;
if (reste === 0) {
clearInterval(id);
console.log("Partez !");
}
}, 1000);
$ node intervalle.mjs
3
2
1
Partez !
9.2.2 La boucle d'événements
Qui exécute la fonction du minuteur, et quand ? Le moteur ne compte pas le temps lui-même : il confie le minuteur au navigateur (ou à Node), qui, le moment venu, dépose la fonction dans une file d'attente.
Une tâche est un morceau de code à exécuter en entier : le script principal, le rappel d'un minuteur, le gestionnaire d'un clic, la réponse d'une requête réseau. Les tâches prêtes attendent dans la file des tâches. La boucle d'événements répète indéfiniment :
- si la pile d'appels est vide, prendre la plus ancienne tâche de la file et l'exécuter jusqu'au bout ;
- exécuter ensuite toutes les microtâches en attente (section 9.3.3) ;
- laisser le navigateur redessiner la page si nécessaire, puis recommencer.
Une tâche n'est jamais interrompue par une autre : c'est ce qu'on appelle l'exécution « jusqu'au bout » (run to completion). Cela simplifie beaucoup la programmation : pas de verrou, pas de synchronized comme en Java, puisque deux morceaux de code ne touchent jamais la même variable en même temps. Le prix à payer est celui de la section précédente : une tâche longue bloque tout.
9.2.3 Les fonctions de rappel, et leur limite
Avant les promesses, on écrivait le code asynchrone avec des fonctions de rappel (callbacks) : on passe à l'opération la fonction à appeler quand elle aura fini. Trois étapes successives (charger un livre, puis son stock, puis ses avis) s'emboîtent alors :
chargerLivre("9782956300014", (livre) => {
chargerStock(livre, (stock) => {
chargerAvis(livre, (avis) => {
console.log(`${livre.titre} : ${stock} en stock, ${avis.length} avis`);
});
});
});
La suite du cours est sur Adloun
Le chapitre en entier, formules et figures comprises, avec le tuteur, les fiches, les QCM et les exercices.
Lire la suite du coursEn accès libre, sans inscription.