K

Utilisation avancée de javascript

Event Loop

L'event loop est le coeur du fonctionnement asynchrone de JavaScript. Comprendre son fonctionnement est essentiel pour débugger et écrire du code performant.

JavaScript est single-threaded

JavaScript n'a qu'un seul thread d'exécution. Il ne peut faire qu'une seule chose à la fois. Alors comment gère-t-il l'asynchrone ?

C'est là qu'intervient l'event loop.

Les composants de l'event loop

1. Call Stack (pile d'appels)

C'est là où JavaScript exécute le code synchrone. Les fonctions sont empilées puis dépilées au fur et à mesure de leur exécution.

function first() {
  console.log("First")
}

function second() {
  first()
  console.log("Second")
}

second()
// Call stack : second() -> first() -> console.log("First") -> console.log("Second")

2. Web APIs / Node APIs

Quand tu appelles setTimeout, fetch, ou des event listeners, ces opérations sont déléguées aux APIs du navigateur (ou de Node). Elles s'exécutent en dehors de la call stack.

3. Callback Queue (Macrotask Queue)

Quand une opération asynchrone est terminée (ex: un setTimeout), son callback est placé dans cette file d'attente.

4. Microtask Queue

Les Promises (.then, .catch, .finally) et queueMicrotask vont dans cette queue prioritaire.

L'ordre d'exécution

Voici la règle fondamentale :

1. Exécuter tout le code synchrone (call stack)
2. Vider TOUTE la microtask queue
3. Exécuter UNE macrotask
4. Retour à l'étape 2

Les microtasks sont TOUJOURS prioritaires sur les macrotasks.

L'exemple classique d'entretien

console.log(1)
setTimeout(() => console.log(2), 0)
Promise.resolve().then(() => console.log(3))
console.log(4)

Exécution pas à pas

  1. console.log(1) - synchrone, s'exécute immédiatement → affiche 1
  2. setTimeout(...) - macrotask, va dans la callback queue
  3. Promise.resolve().then(...) - microtask, va dans la microtask queue
  4. console.log(4) - synchrone, s'exécute immédiatement → affiche 4
  5. Call stack vide, on vide les microtasks → affiche 3
  6. On exécute une macrotask → affiche 2

Résultat : 1, 4, 3, 2

Microtasks vs Macrotasks

Microtasks (prioritaires)

  • Promise.then/catch/finally
  • queueMicrotask()
  • MutationObserver
  • process.nextTick() (Node.js)

Macrotasks

  • setTimeout
  • setInterval
  • setImmediate (Node.js)
  • I/O callbacks
  • UI rendering

Exemple plus complexe

console.log('Start')

setTimeout(() => {
  console.log('Timeout 1')
  Promise.resolve().then(() => console.log('Promise inside timeout'))
}, 0)

Promise.resolve()
  .then(() => console.log('Promise 1'))
  .then(() => console.log('Promise 2'))

setTimeout(() => console.log('Timeout 2'), 0)

console.log('End')

Résultat

Start
End
Promise 1
Promise 2
Timeout 1
Promise inside timeout
Timeout 2

Explication

  1. Synchrone : "Start", "End"
  2. Microtasks : "Promise 1", "Promise 2" (chaînées)
  3. Macrotask 1 : "Timeout 1", puis sa microtask "Promise inside timeout"
  4. Macrotask 2 : "Timeout 2"

Piège classique : setTimeout(fn, 0)

setTimeout(fn, 0) ne veut PAS dire "exécuter immédiatement". Ça veut dire "exécuter dès que possible, après le code synchrone ET les microtasks".

setTimeout(() => console.log('timeout'), 0)
Promise.resolve().then(() => console.log('promise'))
console.log('sync')

// Résultat : sync, promise, timeout

Visualiser l'event loop

Un outil excellent pour visualiser : JavaScript Visualizer 9000

Quiz

Que va afficher ce code ?

async function foo() {
  console.log('foo start')
  await bar()
  console.log('foo end')
}

async function bar() {
  console.log('bar')
}

console.log('script start')
foo()
console.log('script end')

<details> <summary>Réponse</summary>

script start
foo start
bar
script end
foo end

Pourquoi ? await transforme la suite de la fonction en microtask. Donc "foo end" attend que le code synchrone soit terminé.

</details>

En résumé

  1. JavaScript est single-threaded mais gère l'asynchrone via l'event loop
  2. Synchrone d'abord : tout le code de la call stack
  3. Microtasks ensuite : Promises, queueMicrotask (TOUTES)
  4. Macrotasks après : setTimeout, setInterval (UNE à la fois)
  5. setTimeout(fn, 0) ne s'exécute jamais avant les Promises
Précédent
Programmation asynchrone