Boucle d’événements JavaScript : microtâches, macrotâches et pile d’appels
Expliquez comment les promesses, les temporiseurs et les callbacks de fetch se déplacent dans les files d’attente microtask et task, afin que les journaux asynchrones ne semblent plus aléatoires lors des entretiens ou face aux bugs en production.
Sources: Event Loop in JavaScript — JavaScript Interview Questions & Answers — Part 6
Comprendre le cycle d’événements est essentiel pour écrire du JavaScript fiable. Le moteur d’exécution alterne les tâches synchrones sur la pile d’appels avec des appels de retour asynchrones qui arrivent plus tard via les API du système hôte.
Le cycle d’événements planifie ces appels de retour asynchrones une fois que la pile est vide.
console.log("start");
setTimeout(function (){
console.log("timer");
}, 5000);
fetch("https://api.netflix.com")
.then(() => {
console.log("netflix");
});
console.log("end");
console.log("start") s’exécute immédiatement sur la pile, ce qui fait que start s’affiche en premier.
setTimeout enregistre son appel de retour auprès du système hôte (l’API Web du navigateur ou le sous-système de temporisation de Node) et déclenche le retard. JavaScript ne s’arrête pas pendant cinq secondes ; il continue son exécution.
fetch transmet de même la requête réseau au système hôte. Le moteur ne patiente pas en attendant une réponse avant de passer à autre chose. Par conséquent, end s’affiche tandis que des temporisations et des opérations réseau sont encore en cours.
Si le serveur répond en environ une seconde, la fonction de rappel liée à l’exécution de la promesse devient une microtâche et est inscrite dans la file d’attente des microtâches. Les microtâches ont priorité sur les macrotâches (la file d’attente ordinaire des fonctions de rappel/tâches). Une fois que le code synchrone est terminé et que la pile d’appels est vide, le cycle d’événements traite d’abord les microtâches avant de passer à la prochaine macrotâche. Ainsi, le gestionnaire fetch peut s’afficher avant que le délai de cinq secondes ne soit écoulé.
Lorsque le délai d’attente expire, sa fonction de rappel est inscrite dans la file d’attente des tâches. Ce n’est que lorsque la pile est vide et que la file d’attente des microtâches est vide que le cycle d’événements place cette fonction de rappel sur la pile, où elle affiche timer.
File d’attente des microtâches versus file d’attente des fonctions de rappel
Les microtâches ont une priorité plus élevée que la file d’attente des tâches de rappel. Les réactions Promise (.then, .catch, .finally) et les callbacks de MutationObserver sont des microtâches. setTimeout, setInterval ainsi que de nombreux écouteurs d’événements DOM enregistrent des macrotâches dans la file.
Dès que la pile est vide, le boucle traite toutes les microtâches en attente avant d’exécuter une macrotâche. Trois microtâches en file s’exécutent l’une après l’autre avant le prochain délai ou le gestionnaire de clic.
Quand le cycle d’événements débute-t-il ?
Il ne commence pas après l’exécution de votre script. Le cycle est continu pendant l’exécution : il vérifie en permanence si la pile est vide et s’il reste des tâches dans les files.
Seuls les callbacks asynchrones de l’hôte utilisent-ils l’environnement de l’hôte ?
Oui, pour le chemin de mise en file d’attente. Les opérations asynchrones passent par les API du hôte et les files d’attente. Les appels de retour synchrones transmis à map, filter ou reduce s’exécutent directement sur la pile d’appels et ne sont jamais enregistrés dans l’environnement de la Web API comme des tâches différées.
Blocage du thread principal
Tout le JavaScript d’une page (ou du thread principal d’un travailleur Node) partage une seule pile d’appels. Une fonction synchrone longue x() monopolise cette pile : rien d’autre — affichage, clics, autre JavaScript — ne s’exécute tant qu’elle ne renvoie pas de résultat. Préférez les API asynchrones pour les tâches d’attente : compteurs à retardement, réseaux et entrée/sortie de fichiers dans Node, afin que la pile reste disponible pour d’autres tâches.
Web APIs
setTimeout, les outils DOM tels que document.getElementById, fetch, localStorage, et même console ne font pas partie des fonctionnalités de base du langage. Les hébergeurs les exposent en même temps que le moteur JavaScript. Dans les navigateurs, ils dépendent de l’objet global window. Connaître cette frontière — entre le langage et l’hébergeur — permet de comprendre pourquoi il existe un boucle d’événements et des files d’attente : pour relier de manière sécurisée les opérations terminées par l’hébergeur au moteur à thread unique.
Lors des entretiens, on demande souvent ce qui s’affiche lorsque Promise.resolve().then(...) dépasse setTimeout(..., 0). La fonction de rappel de la promesse l’emporte car les microtâches sont entièrement exécutées avant la prochaine macrotâche, même lorsque le délai du chronomètre est nul. Cette règle unique explique une grande partie des erreurs de type « pourquoi cela s’affiche-t-il dans le mauvais ordre ? » dans les codes UI asynchrones.