L'Event Loop
Ce que tu vas apprendre
- L'algorithme complet de l'event loop
- Les 6 phases de l'event loop dans Node.js
- La différence entre l'event loop du navigateur et celle de Node.js
- Comment les callbacks sont planifies et exécutés
- Un walkthrough complet d'une itération de la boucle
Prerequisites
- Avoir lu l'article sur la call stack
- Comprendre la différence entre code synchrone et asynchrone
L'event loop, c'est le coeur battant de JavaScript. C'est le mecanisme qui permet a un langage mono-thread de gerer des milliers d'opérations asynchrones sans bloquer. Et pourtant, la plupart des développeurs JS n'ont qu'une vague idee de comment ca fonctionne.
Je me souviens de ma première tentative d'explication en entretien technique. J'ai dit quelque chose comme "ben, ca tourne en boucle et ca exécuté les callbacks". Le recruteur a hoche la tête poliment. Je n'ai pas eu le poste.
Alors reprenons depuis le début, proprement.
L'idee générale
L'event loop est une boucle infinie qui fait une chose simple : vérifier s'il y a du travail a faire, et si oui, le faire.
+------------------------------------------+
| |
| +--> Call stack vide ? |
| | | |
| | oui v |
| | Microtasks en attente ? ----+ |
| | | oui | |
| | non v | |
| | Macrotask en attente ? | |
| | | | |
| | oui v | |
| | Executer UN macrotask | |
| | | | |
| | v v |
| | Executer TOUTES Executer |
| | les microtasks <--- TOUTES les |
| | | microtasks |
| | v |
| +--- Rendu (navigateur) |
| |
+------------------------------------------+
Deux regles fondamentales :
- Les microtasks sont drainee entièrement entre chaque macrotask
- Un seul macrotask est exécuté par itération (dans le navigateur)
On verra les microtasks et macrotasks en détail dans les prochains articles. Pour l'instant, retiens que setTimeout créé des macrotasks et Promise.then créé des microtasks.
L'event loop dans le navigateur
Dans le navigateur, l'event loop suit la spec HTML (pas ECMAScript). Le cycle simplifie :
- Prendre la plus ancienne tache de la file des macrotasks
- L'exécuter (la call stack se remplit et se vide)
- Exécuter toutes les microtasks en attente
- Si c'est le moment de repaint (~60fps, soit toutes les ~16ms) :
- Exécuter les callbacks
requestAnimationFrame - Faire le rendu (layout, paint, composite)
- Exécuter les callbacks
- Recommencer
Le point 4 est souvent oublie. Si ton code JS bloque la call stack pendant 100ms, le navigateur ne peut pas faire de rendu pendant ce temps. D'ou le gel de l'UI que j'evoquais dans l'introduction.
Les 6 phases de Node.js
Node.js utilise libuv, une bibliothèque C qui implemente son propre event loop. Et cette boucle est plus détaillée que celle du navigateur :
+-------------------------------------------+
| Boucle event loop |
| |
| +-> [1] timers |
| | (setTimeout, setInterval callbacks) |
| | |
| | [2] pending callbacks |
| | (I/O callbacks reportes) |
| | |
| | [3] idle, prepare |
| | (usage interne Node.js) |
| | |
| | [4] poll |
| | (I/O entrant, execution callbacks) |
| | |
| | [5] check |
| | (setImmediate callbacks) |
| | |
| | [6] close callbacks |
| | (socket.on('close'), etc.) |
| | |
| +------- retour au debut --------+ |
| | |
| * microtasks executees ENTRE | |
| chaque phase | |
+------------------------------------+-------+
Phase 1 : timers
Execute les callbacks dont le delai setTimeout ou setInterval a expire. Attention : le delai est un minimum, pas une garantie. Si la boucle est occupee dans la phase poll, les timers seront en retard.
Phase 2 : pending callbacks
Certaines opérations système (erreurs TCP par exemple) reportent leurs callbacks a cette phase plutot que de les exécuter immédiatement dans poll.
Phase 3 : idle, prepare
Usage interne de Node.js. Tu n'interagiras jamais directement avec cette phase. Elle existe pour que libuv puisse faire du menage.
Phase 4 : poll
C'est la phase la plus importante. L'event loop va :
- Calculer combien de temps elle doit attendre pour l'I/O
- Traiter les événements dans la file poll (callbacks de lecture fichier, connexions réseau, etc.)
Si la file poll est vide et qu'il n'y a pas de setImmediate prevu, Node.js va attendre ici que de nouveaux événements arrivent.
Phase 5 : check
Execute les callbacks setImmediate(). Cette phase existe spécifiquement pour permettre d'exécuter du code apres la phase poll, ce que setTimeout(fn, 0) ne garantit pas.
Phase 6 : close callbacks
Gere les callbacks de fermeture comme socket.on('close'). C'est la dernière phase avant de reboucler.
Walkthrough d'une itération
Prenons un exemple concret pour illustrer une page web qui reçoit un clic :
javascriptconsole.log("A");
setTimeout(() => {
console.log("B");
}, 0);
Promise.resolve().then(() => {
console.log("C");
});
console.log("D");
Voici ce qui se passe, pas a pas :
1. Execution synchrone du script (= un macrotask)
Call stack: [script]
-> console.log("A") => affiche "A"
-> setTimeout(cb, 0) => enregistre cb dans la file macrotasks
-> Promise.resolve().then => enregistre then dans la file microtasks
-> console.log("D") => affiche "D"
Call stack: [vide]
2. Drain des microtasks
-> console.log("C") => affiche "C"
File microtasks: [vide]
3. Rendu (si necessaire)
4. Prochain macrotask
-> console.log("B") => affiche "B"
Résultat : A, D, C, B.
Si tu as repondu autre chose, ne t'inquiete pas. On va dissequer ca dans les articles sur les macrotasks et les microtasks. Un site comme paltemps.fr peut servir de terrain d'experimentation pour tester ces mecanismes toi-meme.
Attention : l'event loop n'est PAS un thread
Une confusion courante : l'event loop n'est pas un thread qui tourne en arriere-plan pendant que ton code s'exécuté. L'event loop EST le mecanisme qui exécuté ton code. Quand ta call stack est occupee, l'event loop est "bloquee" -- elle ne peut pas passer a la tache suivante.
C'est pour ca qu'un while(true) gele tout :
javascriptsetTimeout(() => console.log("jamais affiche"), 100);
while (true) {
// l'event loop ne peut jamais atteindre le callback
}
L'event loop ne peut exécuter le callback du timeout que quand la call stack est vide. Si le while ne termine jamais, le callback ne sera jamais exécuté.
Résumé
- L'event loop est une boucle infinie qui orchestre l'exécution du code JS
- Dans le navigateur : macrotask -> microtasks -> rendu -> répéter
- Node.js utilise 6 phases (timers, pending, idle/prepare, poll, check, close)
- Les microtasks sont drainee entre chaque phase / macrotask
- L'event loop n'est pas un thread séparé -- c'est le mecanisme d'exécution lui-meme
- Bloquer la call stack bloque l'event loop et donc tout le reste
Precedent : La Call Stack | Suivant : Les Macrotasks