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
console.log(1)- synchrone, s'exécute immédiatement → affiche 1setTimeout(...)- macrotask, va dans la callback queuePromise.resolve().then(...)- microtask, va dans la microtask queueconsole.log(4)- synchrone, s'exécute immédiatement → affiche 4- Call stack vide, on vide les microtasks → affiche 3
- On exécute une macrotask → affiche 2
Résultat : 1, 4, 3, 2
Microtasks vs Macrotasks
Microtasks (prioritaires)
Promise.then/catch/finallyqueueMicrotask()MutationObserverprocess.nextTick()(Node.js)
Macrotasks
setTimeoutsetIntervalsetImmediate(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
- Synchrone : "Start", "End"
- Microtasks : "Promise 1", "Promise 2" (chaînées)
- Macrotask 1 : "Timeout 1", puis sa microtask "Promise inside timeout"
- 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é
- JavaScript est single-threaded mais gère l'asynchrone via l'event loop
- Synchrone d'abord : tout le code de la call stack
- Microtasks ensuite : Promises, queueMicrotask (TOUTES)
- Macrotasks après : setTimeout, setInterval (UNE à la fois)
- setTimeout(fn, 0) ne s'exécute jamais avant les Promises
