Polar Code 🎭

Command Palette

Search for a command to run...

14
Pièce N°14

Module 14 : L'Event Loop - Le Cerveau de la Nuit

Tu crois que ton code s'exécute ligne par ligne.
Tu te trompes.
JavaScript a un cerveau double : un pour les choses immédiates, un pour les choses qui attendent.
Et entre les deux, une boucle. L'Event Loop.
C'est elle qui décide qui passe, qui attend, qui meurt dans l'ombre.

Comprendre l'Event Loop, c'est comprendre pourquoi ton setTimeout de 0ms n'est pas immédiat.
Pourquoi les Promesses passent avant.
Pourquoi parfois, tout se bloque.


14.1 La Mécanique - Les Trois Pièces du Cerveau

Le Call Stack - La Pile d'Exécution

// Imagine une pile d'assiettes.
// Chaque fonction appelée = une assiette empilée.
// Quand elle finit = l'assiette est retirée.

function premierÉtage() {
  console.log("Début premier étage");
  deuxièmeÉtage(); // Ajoute une assiette
  console.log("Fin premier étage");
}

function deuxièmeÉtage() {
  console.log("Début deuxième étage");
  console.log("Fin deuxième étage");
  // Assiette retirée ici
}

premierÉtage(); // Ajoute la première assiette

// Sortie :
// "Début premier étage"
// "Début deuxième étage"
// "Fin deuxième étage"
// "Fin premier étage"

// LIFO : Last In, First Out.
// Le dernier arrivé est le premier sorti.
// Si la pile est trop haute = "Stack Overflow".

La Task Queue (Macrotask Queue) - La File d'Attente des Gros Travaux

// Pour : setTimeout, setInterval, I/O, UI rendering, events.

console.log("Début");

setTimeout(() => {
  console.log("Timeout 1");
}, 0);

setTimeout(() => {
  console.log("Timeout 2");
}, 0);

console.log("Fin");

// Sortie :
// "Début"
// "Fin"
// "Timeout 1"
// "Timeout 2"

// Les callbacks de setTimeout vont dans cette file.
// L'Event Loop ne les traite que quand la pile est VIDE.

La Microtask Queue - La File Prioritaire

// Pour : Promesses (.then/.catch/.finally), queueMicrotask(), MutationObserver.

console.log("Début");

setTimeout(() => console.log("Timeout"), 0);

Promise.resolve().then(() => console.log("Promesse"));

console.log("Fin");

// Sortie :
// "Début"
// "Fin"
// "Promesse"    ← Microtask (prioritaire)
// "Timeout"     ← Macrotask

// Les microtasks sont traitées IMMÉDIATEMENT après le code synchrone,
// avant les macrotasks.

14.2 L'Ordre Sacré - La Chorégraphie de l'Event Loop

La Danse en 6 Étapes

// 1. Exécuter le code synchrone (call stack)
// 2. Traiter TOUTES les microtasks (jusqu'à ce que la queue soit vide)
// 3. Rendre la page si nécessaire (browser)
// 4. Prendre UNE macrotask de la task queue
// 5. Recommencer à l'étape 1 avec cette macrotask

// Exemple détaillé :
console.log("1 - Sync");

setTimeout(() => console.log("2 - Timeout"), 0);

Promise.resolve()
  .then(() => console.log("3 - Promise 1"))
  .then(() => console.log("4 - Promise chainée"));

queueMicrotask(() => console.log("5 - queueMicrotask"));

console.log("6 - Sync fin");

// Sortie :
// 1 - Sync
// 6 - Sync fin
// 3 - Promise 1
// 4 - Promise chainée
// 5 - queueMicrotask
// 2 - Timeout

Pourquoi cet Ordre ?

// Parce que l'Event Loop donne la PRIORITÉ aux microtasks.
// Elle vide COMPLÈTEMENT la microtask queue avant de passer aux macrotasks.

// Danger : une microtask qui en génère d'autres peut bloquer.
Promise.resolve().then(() => {
  console.log("Microtask 1");
  Promise.resolve().then(() => {
    console.log("Microtask 2");
    Promise.resolve().then(() => {
      console.log("Microtask 3");
      // Ça continue... Les Timeouts attendront indéfiniment.
    });
  });
});

setTimeout(() => console.log("Timeout jamais ?"), 0);
// Le Timeout attendra que TOUTES les microtasks soient finies.

14.3 Promesses vs setTimeout - Le Duel des Files

Le Cas Classique

console.log("Script start");

setTimeout(() => {
  console.log("setTimeout");
}, 0);

Promise.resolve()
  .then(() => {
    console.log("promise1");
  })
  .then(() => {
    console.log("promise2");
  });

console.log("Script end");

// Sortie :
// Script start
// Script end
// promise1
// promise2
// setTimeout

L'Explication Visuelle

[Call Stack Vide]
  ↓
Exécuter tout le code synchrone
  ↓
[Microtask Queue] ← promise1, promise2
[Task Queue]      ← setTimeout callback
  ↓
Vider TOUTE la Microtask Queue
  (promise1 → ajoute promise2 à la queue → la traite)
  ↓
Prendre UNE tâche de la Task Queue
  (setTimeout callback)

Le Piège du setTimeout(fn, 0)

// On l'appelle "setImmediate" mais ce n'est PAS immédiat.

setTimeout(() => console.log("A - Timeout 0"), 0);

Promise.resolve().then(() => console.log("B - Promise"));

for(let i = 0; i < 1000000; i++) {} // Blocage de 100ms

// Sortie :
// B - Promise   (d'abord, car microtask)
// A - Timeout 0 (ensuite, après la boucle)

// Même avec 0ms, setTimeout va dans la Task Queue.
// Il attend que : 1) Le call stack soit vide, 2) La microtask queue soit vide.

queueMicrotask() - Le Contrôle Direct

// Ajoute directement une fonction à la microtask queue.

console.log("Start");

queueMicrotask(() => console.log("Microtask 1"));

setTimeout(() => console.log("Timeout 1"), 0);

Promise.resolve().then(() => console.log("Promise 1"));

queueMicrotask(() => console.log("Microtask 2"));

console.log("End");

// Sortie :
// Start
// End
// Microtask 1
// Promise 1
// Microtask 2
// Timeout 1

14.4 Cas Tordus - Quand l'Ordre Surprend

Cas 1 : setTimeout dans une Promesse

console.log("1");

setTimeout(() => console.log("2"), 0);

Promise.resolve()
  .then(() => {
    console.log("3");
    setTimeout(() => console.log("4"), 0);
  })
  .then(() => console.log("5"));

console.log("6");

// Sortie : 1, 6, 3, 5, 2, 4
// Pourquoi 2 avant 4 ?
// - 2 était déjà dans la Task Queue avant l'exécution des microtasks
// - 4 est ajouté à la Task Queue DURANT l'exécution des microtasks
// - La Task Queue est FIFO (First In, First Out)

Cas 2 : La Promesse Récursive

// ATTENTION : Blocage potentiel !

function microtaskHell() {
  Promise.resolve().then(() => {
    console.log("Microtask");
    microtaskHell(); // Rappelle la fonction immédiatement
  });
}

// microtaskHell(); // Boucle infinie de microtasks
// Les Timeouts ne s'exécuteront JAMAIS.

Cas 3 : await et l'Event Loop

// `await` met en pause, mais ne bloque pas l'Event Loop.

async function test() {
  console.log("1");
  
  await Promise.resolve(); // Pause ici, mais...
  // Le reste devient une microtask
  
  console.log("2");
}

console.log("A");
test();
console.log("B");

// Sortie : A, 1, B, 2
// Explication :
// 1. A (sync)
// 2. Appel test() → 1 (sync dans test)
// 3. await → met le "console.log(2)" dans la microtask queue
// 4. B (sync continue)
// 5. Microtask queue traitée → 2

Cas 4 : setTimeout avec Promesse à l'Intérieur

console.log("Start");

setTimeout(() => {
  console.log("Timeout start");
  
  Promise.resolve().then(() => {
    console.log("Promise inside timeout");
  });
  
  console.log("Timeout end");
}, 0);

Promise.resolve().then(() => {
  console.log("Promise outside");
});

console.log("End");

// Sortie :
// Start
// End
// Promise outside      (microtask avant timeout)
// Timeout start        (première macrotask)
// Timeout end          (toujours dans le callback du timeout)
// Promise inside timeout (microtask AJOUTÉE pendant l'exécution d'une macrotask)

14.5 Règles de Survie - Ne Pas Se Faire Piéger

Règle 1 : Les Microtasks Passent Toujours en Premier

// Après CHAQUE macrotask, l'Event Loop vide TOUTES les microtasks.

setTimeout(() => {
  console.log("Macrotask 1");
  
  Promise.resolve().then(() => console.log("Microtask 1 dans Macrotask 1"));
  
  console.log("Fin Macrotask 1");
}, 0);

setTimeout(() => {
  console.log("Macrotask 2");
}, 0);

// Sortie :
// Macrotask 1
// Fin Macrotask 1
// Microtask 1 dans Macrotask 1  ← Avant Macrotask 2 !
// Macrotask 2

Règle 2 : setImmediate vs setTimeout(0)

// Node.js a `setImmediate` (macrotask) vs `setTimeout(0)` (macrotask)
// Ordre non garanti entre les deux, mais souvent setImmediate en premier.

// Dans le navigateur : `setImmediate` n'existe pas.
// Utilise `queueMicrotask` ou `Promise.resolve()` pour de la vraie immédiateté.

Règle 3 : Éviter le Blocage

// ❌ Mauvais : Microtasks infinies
function process() {
  Promise.resolve().then(process); // Boucle infinie
}

// ✅ Bon : Laisser respirer l'Event Loop
function process() {
  // Faire un peu de travail...
  if (travailRestant) {
    setTimeout(process, 0); // Passe à d'autres tâches d'abord
    // ou
    queueMicrotask(process); // Mais risque de blocage si beaucoup de travail
  }
}

Règle 4 : Comprendre await

// `await` transforme le reste de la fonction en microtask.
// Mais l'appel initial est synchrone jusqu'au premier `await`.

async function test() {
  console.log("A");
  await null; // Transforme le reste en microtask
  console.log("B");
}

console.log("1");
test();
console.log("2");

// Sortie : 1, A, 2, B

14.6 Exercice Mental - Prédire la Sortie

console.log("1");

setTimeout(() => {
  console.log("2");
  Promise.resolve().then(() => console.log("3"));
}, 0);

new Promise((resolve) => {
  console.log("4");
  resolve();
}).then(() => {
  console.log("5");
  setTimeout(() => console.log("6"), 0);
});

queueMicrotask(() => console.log("7"));

console.log("8");

// Essaie de prédire avant de regarder...
<details> <summary>Réponse et explication</summary>
1
4
8
5
7
2
3
6

Explication étape par étape :

  1. console.log("1") → sync
  2. setTimeout(...) → ajoute callback à Task Queue
  3. new Promise executor → console.log("4") → sync
  4. .then(...) → ajoute callback à Microtask Queue
  5. queueMicrotask(...) → ajoute callback à Microtask Queue
  6. console.log("8") → sync
  7. Microtask Queue traitée :
    • Premier microtask (de la promesse) : console.log("5") + ajoute setTimeout à Task Queue
    • Deuxième microtask (queueMicrotask) : console.log("7")
  8. Task Queue traitée (première macrotask) :
    • Callback du premier setTimeout : console.log("2") + ajoute promesse à Microtask Queue
    • Microtask Queue traitée IMMÉDIATEMENT (car on vient de finir une macrotask) : console.log("3")
  9. Task Queue traitée (deuxième macrotask) :
    • Callback du deuxième setTimeout : console.log("6")
</details>

Résumé du Module 14 :

Le Cerveau Triple :

  1. Call Stack : Où le code synchrone s'exécute (LIFO).
  2. Microtask Queue : Pour les Promesses, queueMicrotask(). Prioritaire.
  3. Task Queue : Pour setTimeout, setInterval, events. Secondaire.

L'Event Loop en Boucle :

  1. Exécute le code synchrone jusqu'à ce que la pile soit vide.
  2. Vide TOUTE la Microtask Queue.
  3. Rend la page si nécessaire (browser).
  4. Prend UNE tâche de la Task Queue.
  5. Recommence.

Les Pièges :

  • setTimeout(fn, 0) n'est pas immédiat.
  • Les microtasks peuvent bloquer les macrotasks si elles en génèrent à l'infini.
  • await pause la fonction, mais le reste devient microtask.

Pour ne pas se faire avoir :

  • Utilise queueMicrotask() ou Promise.resolve() pour de la vraie priorité.
  • Évite les boucles infinies de microtasks.
  • Comprends que await ne bloque pas l'Event Loop, il le réorganise.

L'Event Loop est le métronome invisible de JavaScript.
Elle bat, elle choisit, elle ordonne.
Soit tu danses avec elle, soit elle te piétine.