Le piège ici, c'est qu'un appel de fonction async s'exécute de façon
SYNCHRONE jusqu'à son premier await. Il faut donc suivre l'exécution
pas à pas, en distinguant bien code synchrone, appels de fonctions
async, et microtâches créées à chaque await.
Étape 1 — Code synchrone (pile d'appels) :
console.log("P") → P
first() est appelée :
→ console.log("R") → R (exécuté avant le premier await)
→ appelle second() :
→ console.log("O") → O (exécuté avant SON premier await)
→ await Promise.resolve() : second() se suspend ici,
programme sa reprise comme microtâche
→ first() atteint son propre "await second()" : comme second()
est en pause (pas encore résolue), first() se suspend aussi
console.log("M") → M (on revient au flux principal)
setTimeout(..., 0) programmé → macrotâche "E" mise en file
Ordre synchrone : P, R, O, M
Étape 2 — File des microtâches, tour 1 :
La seule microtâche en attente est la reprise de second() après
son "await Promise.resolve()".
→ console.log("I") → I
second() se termine ici, ce qui résout la promesse que first()
attendait. Cela programme une NOUVELLE microtâche (la reprise de
first()) — mais elle passe APRÈS tout ce qui est déjà en file,
donc pas dans ce même tour.
Étape 3 — File des microtâches, tour 2 :
→ console.log("S") → S
first() se termine.
Étape 4 — Macrotâches :
→ console.log("E") → E
Ordre final : P, R, O, M, I, S, E → PROMISE
Le point clé à comprendre : chaque await, même sur une valeur déjà
résolue, coûte un tour COMPLET de microtâche. Et quand une fonction
async B() est awaited par une fonction async A(), la résolution de
la promesse de B() déclenche la reprise de A() comme une microtâche
SUPPLÉMENTAIRE — pas dans le même tour que la fin de B(). C'est cette
cascade en deux temps (I avant S) qui fait toute la difficulté.