JavaScript n'exécute qu'une chose à la fois. Pourtant une page anime un bandeau, écoute cinq boutons et attend trois requêtes sans jamais se figer.
Ce tour de force ne vient pas du langage lui-même, mais du mécanisme qui décide de l'ordre de passage : la boucle d'événements.
Définition
console.log("A");→ 1resetTimeout(() => console.log("B"), 0);→ 4ePromise.resolve().then(() => console.log("C"));→ 3econsole.log("D");→ 2eL'ordre d'affichage n'est pas l'ordre d'écriture. Le script se déroule d'abord en entier, puis la file des microtâches se vide complètement, et une tâche seulement passe ensuite. Un .then inscrit après un setTimeout s'exécute donc avant lui.
Tous les setTimeout portent un délai de zéro, les tâches sont donc servies dans leur ordre d'inscription ; avec des délais différents, c'est l'échéance qui décide entre elles. La démo montre le callback rejoindre la file dès son inscription, alors que l'environnement l'y dépose une fois le minuteur écoulé : un délai de zéro vaut en réalité une milliseconde environ, ce qui ne change rien ici puisque le script se termine avant. Sont également laissés de côté le temps d'exécution, le rendu de la page et les phases supplémentaires de Node.js.
Composez un programme ci-dessus et comparez l'ordre d'écriture à l'ordre de sortie. C'est l'écart entre les deux qui explique la plupart des surprises de l'asynchrone.
La boucle d'événements est le mécanisme qui, en permanence, regarde si la pile d'appels est vide et, si elle l'est, y transfère la prochaine tâche en attente. Trois pièces suffisent à la décrire.
| Pièce | Ce qu'elle contient |
|---|---|
| La pile d'appels | Les fonctions en cours d'exécution, empilées |
| La file des tâches | Les callbacks des minuteurs, des événements, des requêtes |
| La file des microtâches | Les suites de promesses, servies en priorité |
Un setTimeout ne décompte pas dans le moteur : il est confié à l'environnement, navigateur ou Node.js, qui déposera le callback dans la file une fois le délai écoulé.
Un tour de boucle
La règle tient en trois temps : dérouler le code en cours jusqu'au bout, vider entièrement la file des microtâches, puis seulement prendre une tâche.
console.log("1 synchrone");
setTimeout(() => console.log("4 tâche"), 0);
Promise.resolve().then(() => console.log("3 microtâche"));
console.log("2 synchrone");
// 1 synchrone
// 2 synchrone
// 3 microtâche
// 4 tâcheLe délai de zéro milliseconde ne signifie donc pas tout de suite, mais dès que la pile sera libre. Une Microtâche passe systématiquement devant, même inscrite après.
Bloquer la boucle
Puisque tout partage le même fil, une fonction longue empêche tout le reste de se produire : aucun clic n'est traité, aucune animation ne progresse, aucun minuteur ne se déclenche.
const fin = Date.now() + 2000;
while (Date.now() < fin) {} // deux secondes de page gelée
console.log("la page redevient vivante");Un calcul lourd se découpe donc en tranches rendues à la boucle entre deux morceaux, ou part dans un fil séparé. C'est la même contrainte côté serveur : sous Node.js, une fonction bloquante retient toutes les requêtes en cours.
Node.js suit le même principe avec des phases supplémentaires, dont celle des minuteurs et celle des entrées-sorties. D'où l'existence de setImmediate, qui place un callback à la phase suivante sans passer par un délai.
Questions fréquentes
Le code asynchrone s'exécute-t-il en parallèle ?
Non, pas le code JavaScript. L'attente, elle, est bien parallèle : le téléchargement et le décompte d'un minuteur sont pris en charge par l'environnement. Mais les callbacks reviennent tous sur le même fil et passent chacun leur tour.
Pourquoi mon setTimeout à 100 ms se déclenche-t-il plus tard ?
Parce que le délai indique le moment à partir duquel le callback devient éligible, pas le moment de son exécution. Si la pile est occupée quand l'échéance arrive, le callback attend qu'elle se libère. C'est un plancher, jamais une garantie.
Comment sortir un calcul lourd de la boucle ?
Dans un navigateur, un Web Worker exécute le calcul dans un fil séparé et communique par messages. Sous Node.js, la même idée passe par un processus enfant ou un fil de travail. Ces découpages structurent la performance d'un serveur, un sujet travaillé dans la formation Node.js.