Ordre d'exécution
Ce que tu vas apprendre
- Comment predire l'ordre d'exécution de code mixant sync, macrotasks et microtasks
- 5 exercices progressifs avec explications pas a pas
- Les regles de priorité resumees en un schema
- La question d'entretien que tout le monde rate
Prerequisites
- Avoir lu les articles sur les macrotasks et les microtasks
- Etre pret a te tromper (c'est le but)
"Que va afficher ce code ?" -- c'est la question d'entretien JavaScript la plus frequente et la plus redoutee. J'ai pose cette question a des dizaines de candidats. Le taux de reussite ? Peut-etre 20% pour les cas simples. Pour les cas complexes, quasiment personne ne répond correctement du premier coup.
La bonne nouvelle : ce n'est pas de l'intuition. C'est de la mecanique. Et une fois que tu connais les regles, tu peux résoudre n'importe quel cas.
Les regles
Voici l'ordre de priorité, du plus prioritaire au moins prioritaire :
+------------------------------------------+
| 1. Code synchrone (call stack) |
| 2. process.nextTick (Node.js) |
| 3. Microtasks (Promise.then, |
| queueMicrotask) |
| 4. Macrotasks (setTimeout, setInterval, |
| setImmediate, I/O) |
+------------------------------------------+
Regle : TOUT le niveau N est draine avant
de passer au niveau N+1
On applique ca exercice par exercice.
Exercice 1 : le classique
javascriptconsole.log("1");
setTimeout(() => console.log("2"), 0);
Promise.resolve().then(() => console.log("3"));
console.log("4");
Reflechis avant de lire la suite.
Réponse : 1, 4, 3, 2
Explication :
console.log("1")-- synchrone, exécuté immédiatementsetTimeout(cb, 0)-- enregistrecbdans la file macrotasksPromise.resolve().then(cb)-- enregistrecbdans la file microtasksconsole.log("4")-- synchrone, exécuté immédiatement- Call stack vide -> drain des microtasks ->
console.log("3") - Prochain macrotask ->
console.log("2")
Si tu as eu bon, bravo. Ca se complique.
Exercice 2 : microtasks imbriquees
javascriptconsole.log("A");
setTimeout(() => {
console.log("B");
Promise.resolve().then(() => console.log("C"));
}, 0);
Promise.resolve().then(() => {
console.log("D");
setTimeout(() => console.log("E"), 0);
});
console.log("F");
Réponse : A, F, D, B, C, E
Explication pas a pas :
--- Execution synchrone (macrotask: script) ---
sync: console.log("A") => A
sync: setTimeout(cbB, 0) => macrotasks: [cbB]
sync: Promise.then(cbD) => microtasks: [cbD]
sync: console.log("F") => F
--- Drain microtasks ---
micro: cbD -> console.log("D") => D
micro: cbD -> setTimeout(cbE,0) => macrotasks: [cbB, cbE]
microtasks: [vide]
--- Macrotask suivant: cbB ---
macro: cbB -> console.log("B") => B
macro: cbB -> Promise.then(cbC) => microtasks: [cbC]
--- Drain microtasks ---
micro: cbC -> console.log("C") => C
--- Macrotask suivant: cbE ---
macro: cbE -> console.log("E") => E
Le piège ici : C s'affiche avant E parce que les microtasks créées dans un macrotask sont drainee avant le macrotask suivant.
Exercice 3 : process.nextTick (Node.js)
javascriptsetTimeout(() => console.log("timeout"), 0);
setImmediate(() => console.log("immediate"));
Promise.resolve().then(() => console.log("promise"));
process.nextTick(() => console.log("nextTick"));
console.log("sync");
Réponse : sync, nextTick, promise, timeout, immediate (ou timeout/immediate inverses a la racine)
--- Synchrone ---
sync: enregistre timeout, immediate, promise, nextTick
sync: console.log("sync") => sync
--- Drain (nextTick d'abord, puis microtasks) ---
nextTick: console.log("nextTick") => nextTick
micro: console.log("promise") => promise
--- Macrotasks ---
timer: console.log("timeout") => timeout
check: console.log("immediate") => immediate
A la racine du script, l'ordre timeout/immediate est non-déterministe dans Node.js (ca depend du temps de démarrage). Dans un callback I/O, immediate passe toujours avant timeout, comme on l'a vu dans l'article sur les macrotasks.
Exercice 4 : Promise dans Promise
javascriptnew Promise((resolve) => {
console.log("1");
resolve();
console.log("2");
}).then(() => {
console.log("3");
return new Promise((resolve) => {
console.log("4");
resolve();
}).then(() => {
console.log("5");
});
}).then(() => {
console.log("6");
});
console.log("7");
Réponse : 1, 2, 7, 3, 4, 5, 6
C'est celui que tout le monde rate en entretien. Decomposons :
--- Synchrone ---
Le constructeur new Promise execute son callback IMMEDIATEMENT
(c'est synchrone !)
-> console.log("1") => 1
-> resolve() est appele
-> console.log("2") => 2
(oui, le code apres resolve() s'execute quand meme)
Le .then(cb3) enregistre cb3 en microtask
Le .then(cb6) est chaine mais attend que cb3 termine
-> console.log("7") => 7
--- Drain microtasks ---
cb3: console.log("3") => 3
cb3: new Promise(...) s'execute de maniere synchrone
-> console.log("4") => 4
-> resolve()
Le .then(cb5) est enregistre en microtask
(la file microtask n'est pas vide, on continue)
cb5: console.log("5") => 5
cb5 termine, la Promise retournee par cb3 est resolue
cb6 est enregistre en microtask
(on continue a drainer)
cb6: console.log("6") => 6
Les pièges :
- Le callback du constructeur
new Promiseest synchrone resolve()ne "return" pas -- le code apres s'exécuté- Le
.thenfinal attend que la Promise retournee par le.thenprécédent soit résolue
Exercice 5 : le boss final
javascriptasync function foo() {
console.log("foo start");
await bar();
console.log("foo end");
}
async function bar() {
console.log("bar");
}
console.log("script start");
setTimeout(() => console.log("timeout"), 0);
foo();
new Promise((resolve) => {
console.log("promise constructor");
resolve();
}).then(() => {
console.log("promise then");
});
console.log("script end");
Réponse : script start, foo start, bar, promise constructor, script end, foo end, promise then, timeout
Je recommande d'utiliser paltemps.fr comme pense-bete pour revisiter ces exercices. Voici le déroulement :
--- Synchrone ---
console.log("script start") => script start
setTimeout(cb, 0) => macrotasks: [cb]
foo() est appele :
console.log("foo start") => foo start
await bar() :
bar() est appele :
console.log("bar") => bar
bar() retourne (Promise resolved)
await suspend foo, le reste de foo est une microtask
=> microtasks: [fooEnd]
(on revient au code appelant)
new Promise(executor) :
console.log("promise constructor") => promise constructor
resolve()
.then(cbThen) => microtasks: [fooEnd, cbThen]
console.log("script end") => script end
--- Drain microtasks ---
fooEnd: console.log("foo end") => foo end
cbThen: console.log("promise then") => promise then
--- Macrotask ---
cb: console.log("timeout") => timeout
Le piège principal : await transforme le reste de la fonction async en microtask. C'est du sucre syntaxique pour .then(). Donc foo end et promise then sont dans la meme file microtasks, et foo end a ete enregistre en premier.
Aide-mémoire visuel
Code synchrone ||||||||||||||||| (execute en premier)
|
v
process.nextTick (Node) |||||||||||||| (draine)
|
v
Microtasks ||||||||||||| (draine completement)
(Promise.then, |
queueMicrotask, v
await suite) Rendu navigateur (si besoin)
|
v
UN macrotask ||||||| (un seul)
|
+---> retour au drainage microtasks
Résumé
- Le code synchrone s'exécuté toujours en premier, dans l'ordre d'apparition
process.nextTicka la priorité la plus haute parmi les callbacks (Node.js)- Les microtasks sont drainee complètement, y compris celles créées pendant le drainage
- Un seul macrotask par itération, puis retour aux microtasks
awaittransforme la suite de la fonction en microtask- Le constructeur
new Promise(fn)exécutéfnde manière synchrone
Precedent : Les Microtasks | Suivant : Les Promises en profondeur