// js
Qu'est-ce que l'event loop en JavaScript ? L'asynchrone expliqué
JavaScript s'exécute sur un seul thread, et pourtant il gère les timers, le réseau et les clics sans jamais figer la page. L'event loop est la clé : la pile d'appels, les files de tâches et de microtâches, et pourquoi les promesses passent avant setTimeout.
JavaScript a une particularité célèbre : il exécute votre code sur un seul thread, une chose à la fois — et pourtant il peut attendre une requête réseau, faire tourner un timer et répondre aux clics sans jamais se figer. Ce qui rend cela possible, c'est l'event loop. La comprendre, c'est ce qui fait passer le JavaScript asynchrone de déroutant à évident.
Un thread, une pile d'appels
JavaScript exécute le code sur une unique pile d'appels (call stack) : les fonctions y sont empilées quand on les appelle et dépilées quand elles renvoient, une à la fois. Comme il n'y a qu'une seule pile, un seul morceau de code s'exécute à un instant donné. Si une fonction prend du temps, rien d'autre — pas même un clic sur un bouton — ne peut s'exécuter tant qu'elle n'a pas terminé. C'est pourquoi une boucle lourde « fige » la page.
Alors comment JavaScript attend-il un timer de 2 secondes ou un téléchargement lent sans tout bloquer ? L'astuce, c'est qu'il n'attend pas du tout. Il confie ces travaux à l'environnement qui l'entoure et poursuit son exécution.
C'est l'environnement qui attend
Le navigateur (et Node.js) donne à JavaScript des pouvoirs supplémentaires qui ne font pas partie du langage lui-même : timers, requêtes réseau, accès aux fichiers, écouteurs d'événements. Quand vous appelez setTimeout ou fetch, le moteur confie cette tâche à l'environnement et passe immédiatement à la suite. L'environnement attend en arrière-plan, et une fois le travail terminé, il place votre callback dans une file (queue) — et non directement de retour sur la pile.
Ce que fait réellement l'event loop
C'est là qu'intervient l'event loop. Son rôle est simple à énoncer : dès que la pile d'appels est vide, prendre le prochain callback de la file et l'empiler pour l'exécuter. Elle tourne en boucle indéfiniment, vérifiant « la pile est-elle vide ? y a-t-il quelque chose en attente ? » et injectant les callbacks en file un par un. Cette unique règle est tout le mécanisme derrière le JavaScript asynchrone.
La conséquence essentielle : vos callbacks en file ne s'exécutent qu'après que tout le code synchrone en cours est terminé. Un setTimeout(fn, 0) ne s'exécute pas « maintenant » — il s'exécute une fois la pile vidée, même si c'est plus tard que vous ne le pensiez.
Microtâches vs macrotâches : pourquoi les promesses gagnent
Il n'y a pas une seule file mais (au moins) deux, et la différence explique le piège d'entretien le plus courant. Les macrotâches incluent des choses comme setTimeout et les callbacks d'E/S. Les microtâches incluent les callbacks de promesses résolues (.then) et queueMicrotask. La règle est qu'après chaque macrotâche, l'event loop vide entièrement la file des microtâches avant de poursuivre.
C'est pourquoi ceci affiche start, end, promise, timeout — la promesse s'exécute avant le timer, même si celui-ci est réglé à 0 :
console.log('start');
setTimeout(() => console.log('timeout'), 0);
Promise.resolve().then(() => console.log('promise'));
console.log('end'); Les deux appels à console.log sont synchrones et s'exécutent en premier. Le callback de la promesse est une microtâche et s'exécute dès que la pile se vide. Le timeout est une macrotâche et attend son tour après les microtâches.
Pourquoi c'est important en pratique
L'event loop explique les comportements qui font trébucher : pourquoi setTimeout(fn, 0) signifie « dès que possible » et non « immédiatement », pourquoi un long calcul synchrone rend toute la page non réactive, et pourquoi le code async/await ne reprend qu'une fois le travail synchrone environnant terminé. Une fois que vous visualisez la pile qui se vide et la boucle qui injecte les callbacks en file, le JavaScript asynchrone cesse d'être mystérieux.
Node.js vs le navigateur
Le concept est le même dans Node.js, mais l'hôte est différent : au lieu des Web APIs du navigateur, Node utilise une bibliothèque nommée libuv pour gérer les timers, le système de fichiers et le réseau hors du thread principal. La boucle de Node a quelques phases supplémentaires (et process.nextTick se place même avant les microtâches normales), mais le modèle mental est identique — un thread, un environnement qui attend, et une boucle qui réinjecte le travail terminé.
En résumé
L'event loop, c'est la manière dont le JavaScript mono-thread reste réactif : le code synchrone s'exécute sur la pile d'appels, le travail lent est confié à l'environnement, et les callbacks terminés patientent dans des files jusqu'à ce que la pile soit vide. Les microtâches (promesses) s'exécutent avant les macrotâches (timers), et rien d'asynchrone n'interrompt jamais un code déjà en cours d'exécution. Gardez cette image en tête et le reste du JavaScript asynchrone — promesses, async/await, timers — se met en place.
Questions fréquentes
- Qu'est-ce que l'event loop en JavaScript ?
- C'est le mécanisme qui permet à JavaScript, mono-thread, de rester réactif : dès que la pile d'appels est vide, l'event loop prend le prochain callback en file d'attente et l'exécute. Le travail lent (timers, requêtes réseau) est confié à l'environnement, qui met votre callback en file une fois terminé.
- Pourquoi une promesse s'exécute-t-elle avant setTimeout ?
- Les callbacks de promesse sont des microtâches et les timers des macrotâches. Après chaque macrotâche, l'event loop vide d'abord toute la file des microtâches, donc le .then d'une promesse résolue s'exécute avant un callback setTimeout(…, 0), même si le timer était réglé sur zéro.
- setTimeout(fn, 0) s'exécute-t-il immédiatement ?
- Non. Il s'exécute dès que la pile d'appels est vide et après les microtâches en attente, pas instantanément. « 0 » signifie « dès que possible », ce qui reste après la fin du code synchrone en cours.
- L'event loop est-il identique dans Node.js et le navigateur ?
- Le modèle mental est le même — un seul thread, l'environnement attend, et une boucle réinjecte les callbacks terminés. Node.js utilise libuv et a quelques phases supplémentaires (et process.nextTick passe avant les microtâches normales), mais l'idée centrale est identique.