La résolution des fonctions en javascript (call stack, web api, etc.)

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

  1. Les fonctions dans la pile d’appels sont exécutées en premier.
  2. Les callbacks dans la file d’attente des micro-tâches sont ensuite exécutés.
  3. 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

  1. Fonctions simplement synchrones: Dans ce cas, l’ordre d’exécution est simplement l’ordre dans lequel les fonctions apparaissent dans le code.
  2. Promesses simples: Les callbacks then ou catch 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.
  3. setTimeout ou setInterval : 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.
  4. Combinaison de Promesses et setTimeout : Les callbacks des promesses auront toujours la priorité sur setTimeout ou setInterval, car la file d’attente des micro-tâches est vidée avant la file d’attente des macro-tâches.
  5. Utilisation de async/await : Le await fait que la fonction asynchrone attend la résolution de la promesse, mais permet à d’autres codes de s’exécuter. Les instructions après await dans la fonction sont mises dans la file d’attente des micro-tâches.
  6. Promise.resolve() vs Promise.reject() : Si les deux sont dans le même bloc de code, les fonctions then et catch sont mises dans la file d’attente des micro-tâches et sont exécutées en fonction de leur ordre d’apparition.
  7. 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 comme setTimeout.

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');
  1. console.log('1: Début du script'); s’exécute immédiatement, car il est en haut de la pile d’appels.
  2. setTimeout envoie son callback à la Web API et continue.
  3. Promise.resolve ajoute ses callbacks then à la file d’attente des micro-tâches.
  4. console.log('5: Fin du script'); s’exécute, car il est le prochain sur la pile d’appels.
  5. 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); et console.log('4: ', value); s’exécutent.
  6. 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