Back

Comment les Promises JavaScript fonctionnent avec la boucle d'événements

Comment les Promises JavaScript fonctionnent avec la boucle d'événements

Le comportement asynchrone de JavaScript déroute souvent les développeurs. Vous écrivez setTimeout(() => console.log('timeout'), 0) en vous attendant à une exécution immédiate, mais une Promise se résout en premier. Comprendre comment les Promises JavaScript interagissent avec la boucle d’événements révèle pourquoi cela se produit et vous aide à écrire du code asynchrone plus prévisible.

Points clés à retenir

  • La boucle d’événements traite toutes les microtâches avant de passer à la macrotâche suivante
  • Les Promises et async/await utilisent la file des microtâches, obtenant la priorité sur setTimeout
  • Comprendre le système à deux files d’attente aide à prédire l’ordre d’exécution de JavaScript
  • Les microtâches récursives peuvent affamer la boucle d’événements et bloquer les macrotâches

Les fondements de la boucle d’événements JavaScript

JavaScript s’exécute sur un seul thread, traitant une opération à la fois. La boucle d’événements permet les opérations asynchrones en coordonnant la pile d’exécution et deux files d’attente distinctes : la file des microtâches et la file des macrotâches.

La pile d’exécution (call stack) exécute le code synchrone immédiatement. Lorsque la pile se vide, la boucle d’événements vérifie les tâches en attente dans un ordre spécifique :

  1. Toutes les microtâches s’exécutent en premier
  2. Une macrotâche s’exécute
  3. Le cycle se répète

Ce système de priorité explique pourquoi les promises se comportent différemment de setTimeout.

Macrotâche vs Microtâche JavaScript : la différence cruciale

Comprendre les distinctions entre macrotâches et microtâches est essentiel pour prédire l’ordre d’exécution du code.

Les macrotâches incluent :

  • setTimeout
  • setInterval
  • Les opérations d’E/S
  • Le rendu de l’interface utilisateur

Les microtâches incluent :

  • Les callbacks de Promise (.then, .catch, .finally)
  • queueMicrotask()
  • Les callbacks MutationObserver

La boucle d’événements traite toutes les microtâches avant de passer à la macrotâche suivante. Cela crée un système de priorité où les promises s’exécutent toujours avant les timers.

Les Promises et la file des microtâches en action

Examinons comment les promises interagissent avec la boucle d’événements à travers du code :

console.log('1');

setTimeout(() => console.log('2'), 0);

Promise.resolve()
  .then(() => console.log('3'))
  .then(() => console.log('4'));

console.log('5');

Résultat : 1, 5, 3, 4, 2

Voici le flux d’exécution :

  1. Le console.log('1') synchrone s’exécute immédiatement
  2. setTimeout planifie le callback dans la file des macrotâches
  3. Les callbacks de Promise sont mis en file d’attente comme microtâches
  4. Le console.log('5') synchrone s’exécute
  5. La boucle d’événements traite toutes les microtâches (3, 4)
  6. La boucle d’événements traite une macrotâche (2)

La file des microtâches se vide complètement avant l’exécution de toute macrotâche, même avec des timeouts à délai zéro.

Intégration d’Async/Await et de la boucle d’événements

Le comportement d’async/await suit les mêmes règles de microtâches. Le mot-clé await met en pause l’exécution de la fonction et planifie la continuation comme une microtâche :

async function example() {
  console.log('1');
  await Promise.resolve();
  console.log('2');  // Ceci devient une microtâche
}

example();
console.log('3');

// Résultat : 1, 3, 2

Après await, le reste du corps de la fonction rejoint la file des microtâches. Cela explique pourquoi console.log('3') s’exécute avant console.log('2') bien qu’il apparaisse plus tard dans le code.

Pièges courants et patterns pratiques

Affamement de la file des microtâches

Créer des microtâches de manière récursive peut bloquer la boucle d’événements :

function dangerousLoop() {
  Promise.resolve().then(dangerousLoop);
}
// Ne faites pas cela - bloque toutes les macrotâches

Mélanger les timers avec les Promises

Lors de la combinaison de différents patterns asynchrones, rappelez-vous la priorité d’exécution :

setTimeout(() => console.log('timeout'), 0);

fetch('/api/data')
  .then(response => response.json())
  .then(data => console.log('data'));

Promise.resolve().then(() => console.log('immediate'));

// Ordre : immediate → data (quand prêt) → timeout

Déboguer l’ordre d’exécution

Utilisez ce pattern pour tracer le flux d’exécution :

console.log('Sync start');

queueMicrotask(() => console.log('Microtask 1'));

setTimeout(() => console.log('Macrotask 1'), 0);

Promise.resolve()
  .then(() => console.log('Microtask 2'))
  .then(() => console.log('Microtask 3'));

setTimeout(() => console.log('Macrotask 2'), 0);

console.log('Sync end');

// Résultat : Sync start, Sync end, Microtask 1, Microtask 2, Microtask 3, Macrotask 1, Macrotask 2

Conclusion

Le système à deux files d’attente de la boucle d’événements détermine l’ordre d’exécution asynchrone de JavaScript. Les Promises et async/await utilisent la file des microtâches, obtenant la priorité sur setTimeout et autres macrotâches. Cette connaissance transforme un comportement asynchrone mystérieux en patterns prévisibles, vous permettant d’écrire du code JavaScript asynchrone plus fiable.

Rappelez-vous : le code synchrone s’exécute en premier, puis toutes les microtâches se vident, puis une macrotâche s’exécute. Ce cycle se répète, permettant au thread unique de JavaScript de gérer efficacement des opérations asynchrones complexes.

FAQ

Les callbacks de Promise sont des microtâches tandis que setTimeout crée des macrotâches. La boucle d'événements traite toujours toutes les microtâches avant de passer à la macrotâche suivante, quelle que soit la durée du timeout.

Oui, créer continuellement des promises ou des microtâches sans permettre aux macrotâches de s'exécuter peut affamer la boucle d'événements. Cela empêche les mises à jour de l'interface utilisateur et d'autres macrotâches de s'exécuter.

Async/await est du sucre syntaxique pour les promises. Le mot-clé await met en pause l'exécution et planifie la continuation comme une microtâche, suivant les mêmes règles de priorité que les callbacks de promise.

Les différents patterns suivent leurs règles de file d'attente. Les Promises et async/await utilisent les microtâches, tandis que setTimeout et setInterval utilisent les macrotâches. Les microtâches s'exécutent toujours en premier lorsque la pile d'exécution est vide.

Complete picture for complete understanding

Capture every clue your frontend is leaving so you can instantly get to the root cause of any issue with OpenReplay — the open-source session replay tool for developers. Self-host it in minutes, and have complete control over your customer data.

Check our GitHub repo and join the thousands of developers in our community.

OpenReplay