Async JavaScript - 02 - L'Event Loop

L'algorithme de l'event loop, les phases de Node.js, la boucle du navigateur, et comment les callbacks sont planifies.

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 :

  1. Les microtasks sont drainee entièrement entre chaque macrotask
  2. 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 :

  1. Prendre la plus ancienne tache de la file des macrotasks
  2. L'exécuter (la call stack se remplit et se vide)
  3. Exécuter toutes les microtasks en attente
  4. Si c'est le moment de repaint (~60fps, soit toutes les ~16ms) :
    • Exécuter les callbacks requestAnimationFrame
    • Faire le rendu (layout, paint, composite)
  5. 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

Sources

Réservez un audit gratuit de 30 minutes. Je vous montre concrètement ce qu'on peut automatiser.