Pour comprendre en profondeur le cycle de vie des promesses et quel callback est activé avant quel autre, il est crucial de connaître les concepts de la pile d’appels (Call Stack), de l’API Web, de la file d’attente des macro-tâches (Macro Task Queue) et de la file d’attente des micro-tâches (Microtask Queue) dans l’environnement JavaScript. Nous allons voir d’abord ces différents éléments de la résolution des fonctions javascript, avant d’étudier les différents cas de figure.
Les éléments de la résolution de fonction en javascript
Pile d’appels (Call Stack)
La pile d’appels gère l’exécution des fonctions en suivant le principe LIFO (Last In, First Out). Lorsqu’une fonction est appelée, elle est ajoutée à la pile. Une fois que la fonction a terminé son exécution, elle est retirée de la pile. En général, les fonctions vont entrer et aussitôt « ressortir » de la pile, mais il peut arriver qu’elles s’accumulent, par exemple lorsqu’il y a plusieurs fonctions imbriquées.
Web API
Le navigateur fournit des fonctionnalités asynchrones via la Web API, comme setTimeout
, fetch
, etc. Lorsque ces fonctions sont appelées, elles sont sorties de la pile d’appels et placées dans la Web API pour être exécutées en arrière-plan.
File d’attente des macro-tâches (Macro Task Queue)
C’est une file d’attente qui stocke les callbacks des tâches asynchrones telles que setTimeout
, setInterval
et les événements DOM. Une fois que la Web API a terminé de traiter une tâche, le callback correspondant est placé dans cette file d’attente.
File d’attente des micro-tâches (Microtask Queue)
Contrairement à la file d’attente des macro-tâches, cette file d’attente stocke les callbacks des promesses. Les micro-tâches ont une priorité plus élevée que les macro-tâches.
L’Event Loop
L’Event Loop est le mécanisme qui coordonne ces différentes composantes. Il tourne en boucle et a la responsabilité de vérifier la pile d’appels et les files d’attente pour décider quel code doit être exécuté ensuite. Il donne la priorité aux micro-tâches sur les macro-tâches, ce qui a des implications importantes sur l’ordre d’exécution des callbacks.
Ordre d’Exécution
- Les fonctions dans la pile d’appels sont exécutées en premier.
- Les callbacks dans la file d’attente des micro-tâches sont ensuite exécutés.
- Enfin, les callbacks dans la file d’attente des macro-tâches sont exécutés.
Les différents cas de figure
L’ordre de résolution en principe
- Fonctions simplement synchrones: Dans ce cas, l’ordre d’exécution est simplement l’ordre dans lequel les fonctions apparaissent dans le code.
- Promesses simples: Les callbacks
then
oucatch
des promesses sont mis dans la file d’attente des micro-tâches et exécutés après que le code synchrone ait fini d’exécuter et que la pile d’appels soit vide. setTimeout
ousetInterval
: Leur callback est mis dans la file d’attente des macro-tâches et ne s’exécutera qu’après que toutes les micro-tâches et le code synchrone ont été exécutés.- Combinaison de Promesses et
setTimeout
: Les callbacks des promesses auront toujours la priorité sursetTimeout
ousetInterval
, car la file d’attente des micro-tâches est vidée avant la file d’attente des macro-tâches. - Utilisation de
async/await
: Leawait
fait que la fonction asynchrone attend la résolution de la promesse, mais permet à d’autres codes de s’exécuter. Les instructions aprèsawait
dans la fonction sont mises dans la file d’attente des micro-tâches. Promise.resolve()
vsPromise.reject()
: Si les deux sont dans le même bloc de code, les fonctionsthen
etcatch
sont mises dans la file d’attente des micro-tâches et sont exécutées en fonction de leur ordre d’apparition.- Queue des événements (Event Loop) : Dans un environnement de navigateur, l’ordre peut aussi être influencé par des événements comme les clics de souris ou les entrées de clavier. Ces événements sont également placés dans la file d’attente des macro-tâches.
Les éléments imbriqués dans d’autres devront attendre la résolution Callbacks imbriqués : Si un setTimeout
ou une promesse est défini à l’intérieur d’un autre setTimeout
ou then
, il doit attendre que la première macro-tâche ou micro-tâche soit terminée.
Cas particuliers
- Les callbacks de promesse (
then
,catch
,finally
) sont ajoutés à la file d’attente des micro-tâches et sont donc exécutés avant les callbacks des macro-tâches commesetTimeout
.
Exemple
console.log('1: Début du script');
setTimeout(() => {
console.log('2: setTimeout callback');
}, 0);
Promise.resolve('Promesse résolue')
.then(value => {
console.log('3: ', value);
return Promise.resolve('Deuxième promesse résolue');
})
.then(value => {
console.log('4: ', value);
});
console.log('5: Fin du script');
console.log('1: Début du script');
s’exécute immédiatement, car il est en haut de la pile d’appels.setTimeout
envoie son callback à la Web API et continue.Promise.resolve
ajoute ses callbacksthen
à la file d’attente des micro-tâches.console.log('5: Fin du script');
s’exécute, car il est le prochain sur la pile d’appels.- La pile d’appels étant vide, les callbacks de la file d’attente des micro-tâches sont exécutés. Les
console.log('3: ', value);
etconsole.log('4: ', value);
s’exécutent. - Enfin, le callback de
setTimeout
est exécuté, car il est dans la file d’attente des macro-tâches et toutes les micro-tâches ont été exécutées.
Le résultat est le suivant:
1: Début du script
5: Fin du script
3: Promesse résolue
4: Deuxième promesse résolue
2: setTimeout callback
Exemple vidéo ci-contre
- Une vidéo en anglais extrêmement complète : Asynchronous JavaScript & EVENT LOOP from scratch 🔥 | Namaste JavaScript Ep.15