Boucle d’événements JavaScript expliquée : pile d’appels, microtâches et minuteries avec des exemples

  1. Accueil
  2. Blog
  3. Développement de logiciels

Développement de logiciels

Boucle d’événements JavaScript expliquée : Pile d’appels, microtâches et minuteries avec exemples

Pourquoi un setTimeout de zéro s’exécute-t-il toujours en dernier ? Comment un thread peut-il gérer autant de choses à la fois ? Découvrez comment fonctionne réellement la boucle d’événements, testez-la avec le code que nous avons exécuté et résolvez la question d’entretien classique.

9 min de lecture
Boucle d'événements JavaScript expliquée : pile d'appels, microtâches et minuteries avec exemples

À retenir

  • JavaScript exécute votre code sur un un seul thread, une pièce à la fois.
  • Lorsque la pile d’appels est vide, toutes les microtâches s’exécutent en premier, puis une tâche.
  • setTimeout(fn, 0) signifie « dès que possible après un délai minimum », et non « maintenant ».
  • Un long travail synchrone bloque tout, alors divisez-le ou déplacez-le vers un travailleur.

Exécutez quelques lignes avec setTimeout et une promesse, et l’ordre de sortie pourrait vous surprendre. La boucle d’événements JavaScript explique pourquoi. C’est le mécanisme qui permet à un langage monothread de gérer les minuteries, les appels réseau et les clics sans se figer.

Une fois que vous l’avez compris, toute une classe de bugs déroutants commence à avoir un sens. Nous avons exécuté chaque exemple sur Node.js 24, et le résultat provient de cette exécution. Les règles de classement de base s’appliquent également dans les navigateurs, et nous soulignons les différences entre Node.js. Vous en tirerez davantage si vous connaissez déjà les bases des Promesses JavaScript.

Pourquoi la boucle d’événements existe

JavaScript exécute votre code sur un thread principal, de sorte qu’un seul élément s’exécute à la fois. S’il attendait simplement une requête réseau, une page se figerait jusqu’à ce que la réponse revienne.

Donc JavaScript n’attend pas. Le travail lent est confié à l’environnement hôte, c’est-à-dire au navigateur ou à Node.js, qui peut le gérer en arrière-plan. Une fois ce travail terminé, un rappel est placé dans une file d’attente. Le travail de la boucle d’événements consiste à déplacer les rappels en file d’attente sur la pile lorsqu’elle est libre.

Pensez à un chef travaillant seul. Le chef met un plat au four et règle un minuteur, plutôt que de le regarder. Lorsque le minuteur sonne, la tâche est inscrite sur une liste et le chef la récupère après avoir terminé ce qui est actuellement en cours.

Les pièces mobiles

Partie Ce que ça fait Exemples
Pile d’appels Suit les fonctions en cours d’exécution Votre code synchrone
API hôtes Travaillez en dehors de votre code Minuteries, fetch, lectures de fichiers
File d’attente des tâches Contient les rappels prêts à être exécutés setTimeout, clics, E/S
File d’attente de microtâches Contient les rappels de priorité plus élevée .then, await, queueMicrotask
Boucle d’événement Décide de ce qui se passera ensuite Fonctionne en continu

Les tâches sont parfois appelées macrotâches. À proprement parler, il peut y avoir plusieurs sources de tâches, et les détails exacts diffèrent selon les environnements.

Les règles

  1. Exécutez tout le code synchrone jusqu’à ce que la pile d’appels soit vide.
  2. Exécutez toutes les microtâches, y compris les nouvelles ajoutées en cours de route.
  3. Dans un navigateur, effectuez une mise à jour si une mise à jour est due.
  4. Prenez une tâche dans une file d’attente et exécutez-la.
  5. Revenez à l’étape 2.

Le détail le plus important : toutes microtâches, puis une tâche. La plupart des énigmes de boucles d’événements sont résolues en appliquant cette seule règle.

Voir la commande

console.log("1 sync");
setTimeout(() => console.log("5 timeout"), 0);
Promise.resolve().then(() => console.log("3 promise"));
queueMicrotask(() => console.log("4 microtask"));
console.log("2 sync");

// 1 sync
// 2 sync
// 3 promise
// 4 microtask
// 5 timeout

Voici ce qui s’est passé. Les deux lignes console.log sont synchrones, elles ont donc été exécutées en premier. Le rappel de promesse et queueMicrotask sont allés dans la file d’attente des microtâches et ils ont été exécutés dès que la pile était vide, dans l’ordre dans lequel ils ont été ajoutés. Le rappel du minuteur devait attendre le tour d’une tâche, même si son délai était nul.

Les microtâches s’exécutent entre les minuteries

Après chaque tâche, la file d’attente des microtâches est à nouveau vidée. Cet exemple le prouve :

setTimeout(() => {
  console.log("timer 1");
  Promise.resolve().then(() => console.log("promise inside timer 1"));
}, 0);
setTimeout(() => console.log("timer 2"), 0);

// timer 1
// promise inside timer 1
// timer 2

La promesse, qui a été créée lors du premier minuteur, a été exécutée avant le deuxième minuteur. Les navigateurs modernes et Node.js 11 et versions ultérieures se comportent de cette façon.

Où l’attente correspond

await divise une fonction en deux. Tout avant son exécution de manière synchrone et tout ce qui suit est planifié sous forme de microtâche. Cela surprend beaucoup de gens :

async function task() {
  console.log("A before await");
  await null;
  console.log("C after await");
}

task();
console.log("B after calling task");

// A before await
// B after calling task
// C after await

Une fonction async ne démarre pas en arrière-plan. Il s’exécute sur le même thread jusqu’à ce qu’il atteigne son premier await. Cela s’applique même lorsque l’on attend quelque chose qui n’est pas une vraie promesse, comme null. Consultez notre guide JavaScript async/await pour en savoir plus sur cette syntaxe.

Bloquer la boucle

Pendant l’exécution du code synchrone, rien d’autre ne peut le faire. Pour montrer cela, nous avons réglé un timer sur 0 ms, puis avons gardé le thread occupé pendant une demi-seconde :

const start = Date.now();
setTimeout(() => console.log("timer ran after", Date.now() - start, "ms"), 0);
while (Date.now() - start < 500) {}

// timer ran after about 500 ms

Le délai du minuteur est un minimum et non une garantie. Il ne fonctionnait que lorsque la pile devenait libre. Sur une page Web, une longue boucle comme celle-ci signifie que les clics ne répondent pas et que les animations s’arrêtent. Sur un serveur, toutes les autres requêtes doivent attendre, c’est pourquoi un appel synchrone lent peut nuire à tous les utilisateurs d’un processus Node.js.

Les causes courantes de blocage incluent d’énormes boucles, l’analyse de JSON très volumineux, des fonctions de fichiers synchrones comme readFileSync dans les gestionnaires de requêtes et des expressions régulières catastrophiques.

Astuce : Dans Chrome DevTools, le panneau Performances marque les tâches de plus de 50 ms comme des tâches longues. C’est un moyen rapide de trouver ce qui rend une page lente.

Rester réactif

Pour les travaux lourds, donnez à la boucle d’événements une chance de respirer. Une solution consiste à faire une pause sur une minuterie de temps en temps, ce qui permet à d’autres tâches de s’exécuter :

const items = [0, 1, 2, 3, 4];

async function processAll() {
  for (const item of items) {
    if (item === 2) await new Promise((r) => setTimeout(r, 0));
    console.log("item", item);
  }
}

setTimeout(() => console.log("other work gets a turn"), 0);
processAll();

// item 0
// item 1
// other work gets a turn
// item 2
// item 3
// item 4

L’autre chronomètre s’est déroulé au milieu, car nous avons cédé face à une vraie tâche. Le vrai code produirait un nombre d’éléments ou de millisecondes. Pour les travaux vraiment gourmands en CPU, tels que le traitement d’images, utilisez un Web Worker dans les navigateurs ou un thread de travail dans Node.js, qui s’exécutent sur des threads distincts.

Manque de microtâches

Étant donné que la boucle vide complètement la file d’attente des microtâches avant de continuer, une microtâche qui continue de planifier davantage peut arrêter tout le reste. Nous avons testé cela avec 100 000 microtâches enchaînées :

let count = 0;
let timerFired = false;
setTimeout(() => { timerFired = true; }, 0);

function spin() {
  count++;
  if (count < 100000) queueMicrotask(spin);
  else console.log("timer fired during microtasks?", timerFired);
}
queueMicrotask(spin);

// timer fired during microtasks? false

Le chronomètre ne pouvait pas fonctionner tant que tous n’étaient pas terminés. Si cette chaîne ne se terminait jamais, les minuteries ne se déclencheraient jamais et, dans un navigateur, la page ne serait jamais repeinte. Notez que céder avec await sur une promesse résolue n’aide pas ici, car cela ajoute simplement une autre microtâche. Vous avez besoin d’une vraie tâche, comme un chronomètre, pour donner le tour aux autres.

Rendu dans les navigateurs

Un navigateur a généralement la possibilité de peindre entre les tâches. Cela explique un bug courant :

spinner.hidden = false;
doHeavyWork();              // blocks the thread
spinner.hidden = true;

Le spinner n’apparaît jamais, car le navigateur n’a pas eu la possibilité d’effectuer un rendu entre ces lignes. Pour y remédier, laissez passer un cadre avant le début des gros travaux, ou confiez ce travail à un ouvrier. Pour le code qui modifie les visuels, requestAnimationFrame s’exécute juste avant la prochaine peinture, ce qui en fait le bon outil pour l’animation.

Différences Node.js

Node.js possède sa propre boucle d’événements avec plusieurs phases, notamment des minuteries, des interrogations d’E/S et setImmediate. Il dispose également de process.nextTick, qui s’exécute dans sa propre file d’attente. Voici ce que nous avons vu lors de leur exécution depuis une fonction asynchrone :

setTimeout(() => console.log("timeout"), 0);
setImmediate(() => console.log("immediate"));
process.nextTick(() => console.log("nextTick"));
Promise.resolve().then(() => console.log("promise"));

// promise
// nextTick
// immediate
// timeout

De nombreux articles affirment que nextTick bat toujours les promesses et qu’un minuteur bat setImmediate. Notre résultat montre que ce n’est pas si simple. Notre code s’exécutait déjà dans une microtâche, donc la file d’attente des promesses s’est poursuivie en premier. Et l’ordre de setTimeout et setImmediate est connu pour varier en fonction de l’endroit d’où ils sont appelés. N’écrivez donc pas de code qui dépend de l’ordre exact de ceux-ci. Si une chose doit se produire après une autre, enchaînez-les explicitement.

Quiz d’entretien

Essayez de déterminer l’ordre avant de lire la réponse :

console.log("a");
setTimeout(() => console.log("b"), 0);
Promise.resolve().then(() => {
  console.log("c");
  setTimeout(() => console.log("d"), 0);
  Promise.resolve().then(() => console.log("e"));
});
console.log("f");

Notre exécution a imprimé a, f, c, e, b, d. Voici pourquoi :

  1. a et f sont synchrones, ils s’exécutent donc en premier.
  2. La pile est vide, donc les microtâches s’exécutent : c, puis la nouvelle microtâche e, dans le même tour.
  3. Maintenant une tâche : le minuteur b, qui a été mis en file d’attente en premier.
  4. Puis la tâche suivante : le timer d, qui a été mis en file d’attente pendant la microtâche.

Erreurs courantes

  • La réflexion setTimeout est exacte. Le délai n’est qu’un minimum.
  • Penser async signifie un autre thread. Le code asynchrone s’exécute toujours sur le thread principal entre les attentes.
  • Utiliser les mises en veille pour corriger l’ordre. Attendre la condition réelle est beaucoup plus fiable, ce qui s’applique également aux tests irréguliers.
  • Utilisation de forEach avec await. Il n’attend pas entre les éléments. Voir Méthodes de tableau JavaScript.
  • Effectuer un travail synchrone lourd dans un gestionnaire de serveur. Une requête peut retarder toutes les autres requêtes.

Pour conclure

N’oubliez pas la règle de base : exécutez la pile jusqu’à ce qu’elle soit vide, exécutez toutes les microtâches, puis exécutez une tâche et répétez. Tout le reste en découle. Pour que cela dure, écrivez votre propre version du quiz ci-dessus, prédisez le résultat sur papier, puis exécutez-le. Se tromper et comprendre pourquoi vous en apprendra plus que n’importe quel diagramme.

Questions fréquentes

Qu’est-ce que la boucle d’événements en JavaScript ?

C’est le mécanisme qui exécute les rappels en file d’attente lorsque la pile d’appels est vide. Il permet à JavaScript monothread de gérer les minuteries, les entrées et les réponses réseau sans blocage.

Quelle est la différence entre une microtâche et une tâche ?

Les microtâches, telles que les rappels de promesses, s’exécutent toutes dès que la pile est vide. Les tâches, telles que les minuteries, s’exécutent une par une, les microtâches étant épuisées après chacune.

Pourquoi setTimeout avec 0 ne s’exécute-t-il pas immédiatement ?

Son rappel doit attendre un tour de tâche. Tous les codes et microtâches synchrones s’exécutent en premier, et le délai est un minimum plutôt qu’une heure exacte.

JavaScript est-il monothread ?

Votre code JavaScript principal s’exécute sur un seul thread. Les navigateurs et Node.js utilisent d’autres threads pour certains travaux en arrière-plan, et les Workers vous permettent d’exécuter du code en parallèle.

Wait bloque-t-il la boucle d’événements ?

Non, wait met en pause uniquement cette fonction asynchrone, et d’autres codes peuvent s’exécuter entre-temps. Cependant, un code synchrone long avant ou après peut toujours bloquer la boucle.

Continuez à apprendre

Travailler avec Stack Quora

Besoin d’aide pour les tests, WordPress ou le référencement ?

Nous aidons les équipes à proposer de meilleurs logiciels et à développer leurs sites. Dites-nous sur quoi vous travaillez et nous vous proposerons une prochaine étape claire.

Continuez à lire

Plus de guides sur tests, IA et développement moderne

Vous avez apprécié cet article ? Choisissez un sujet et accédez directement à nos nouveaux didacticiels.

Intelligence artificielle

Les hallucinations de l'IA expliquées : pourquoi les modèles de langage inventent les choses et comment les réduire

Les hallucinations de l'IA ont expliqué : ce qu'elles sont, pourquoi les modèles de langage les produisent, les types courants, comment les réduire …

Lire l'article →
DevOps

Variables d'environnement expliquées : fichiers .env, secrets et configuration correctement effectués

Variables d'environnement expliquées avec des exemples Python que nous avons exécutés : lecture et définition, pourquoi chaque valeur est une chaîne…

Lire l'article →
Développement logiciel

Limitation du débit de l'API expliquée : algorithmes, en-têtes et gestion des erreurs 429

Limitation du débit de l'API expliquée avec une simulation que nous avons exécutée : compartiment de jetons, fenêtre fixe, fenêtre glissante, 429 ré…

Lire l'article →
Automatisation des tests

Se moquer en Python : unittest.mock, patch et les erreurs qui font mentir les tests

Moquerie en Python avec unittest.mock, testée : Mock et return_value, side_effect, correctifs où un nom est utilisé, spec et autospec, dates moqueus…

Lire l'article →
Tests logiciels

Couverture du code expliquée : couverture des lignes par rapport aux branches et ce que signifie réellement 100 %

Couverture du code expliquée avec une véritable exécution de cover.py : couverture des lignes, des branches et des conditions, lecture d'un rapport,…

Lire l'article →
Bases de données et SQL

DELETE vs TRUNCATE vs DROP dans SQL : différences, restauration et quand les utiliser

DELETE vs TRUNCATE vs DROP expliqué avec SQL testé : ce que chacun supprime, restauration, déclencheurs, réinitialisation d'identité, vitesse et en …

Lire l'article →
Bases de données et SQL

Vues SQL expliquées : créez, utilisez et évitez les pièges, avec des exemples

Vues SQL expliquées avec les requêtes que nous avons exécutées : création d'une vue, résultats en direct, masquage des colonnes, performances, vues …

Lire l'article →
Développement logiciel

Interface TypeScript vs Type : différences réelles, testées avec le compilateur

Interface TypeScript vs type, vérifiée avec le vrai compilateur : ce que les deux peuvent faire, ce qu'un seul peut faire, la fusion des déclaration…

Lire l'article →
Développement logiciel

Python *args et **kwargs expliqués : fonctions flexibles avec des exemples clairs

Python *args et **kwargs expliqués avec le code que nous avons exécuté : emballage et déballage, ordre des paramètres, arguments de mot-clé uniqueme…

Lire l'article →
Développement logiciel

Fonctions Python Lambda : syntaxe, utilisations réelles et quand ne pas les utiliser

Fonctions Python lambda expliquées avec le code que nous avons exécuté : syntaxe, tri avec clé, carte et filtre, fermetures, piège de liaison tardiv…

Lire l'article →
Intelligence artificielle

Que sont les agents IA ? Comment ils fonctionnent, avec une simple boucle d'agent que vous pouvez exécuter

Que sont les agents IA ? Un guide simple sur la boucle d'agent, les outils, la mémoire et les garde-fous, avec une petite boucle Python que nous avo…

Lire l'article →
DevOps

Surveillance vs observabilité : la vraie différence, avec les journaux, les métriques et les traces

Surveillance et observabilité expliquées : échecs connus et inconnus, journaux, métriques et traces, présentation pas à pas des incidents de paiemen…

Lire l'article →
DevOps

Volumes Docker vs montages Bind : conserver les données du conteneur de la bonne manière

Volumes Docker et montages de liaison expliqués : pourquoi les données du conteneur disparaissent, volumes nommés, montages de liaison, tmpfs, exemp…

Lire l'article →
Automatisation des tests

TDD vs BDD : principales différences, une fonctionnalité conçue dans les deux sens et quand les utiliser

TDD vs BDD expliqué avec une fonctionnalité de remise construite dans les deux sens : refactor rouge-vert avec le code que nous avons exécuté, scéna…

Lire l'article →
Tests logiciels

Tests d'acceptation utilisateur (UAT) : processus, liste de contrôle et exemples réels

Les tests d'acceptation des utilisateurs sont expliqués : qui les exécute, en quoi ils diffèrent du contrôle qualité, un processus en sept étapes, u…

Lire l'article →
Bases de données et SQL

Explication des valeurs SQL NULL : IS NULL, COALESCE, NULLIF et les pièges à éviter

Les valeurs SQL NULL expliquées avec les requêtes que nous avons exécutées : logique à trois valeurs, IS NULL, COALESCE, NULLIF, comment les agrégat…

Lire l'article →
Bases de données et SQL

Instruction SQL CASE WHEN : syntaxe, exemples réels et erreurs courantes

SQL CASE WHEN expliqué avec les requêtes que nous avons exécutées : formulaires simples et recherchés, ordre des conditions, pivots avec SUM, astuce…

Lire l'article →
Développement logiciel

Qu'est-ce que JWT ? Les jetons Web JSON expliqués avec un exemple fonctionnel

Qu’est-ce que JWT ? JSON Web Tokens expliqué : les trois parties, un jeton construit et falsifié dans le code que nous avons exécuté, JWT vs session…

Lire l'article →
Développement logiciel

Boucle d'événements JavaScript expliquée : pile d'appels, microtâches et minuteries avec des exemples

La boucle d'événements JavaScript expliquée avec le code que nous avons exécuté : pile d'appels, files d'attente de tâches et de microtâches, pourqu…

Lire l'article →
Développement logiciel

Générateurs Python et rendement expliqués : économisez de la mémoire avec des exemples réels

Les générateurs Python et le rendement expliqués avec le code que nous avons exécuté : comment ils s'arrêtent et reprennent, les économies de mémoir…

Lire l'article →
Intelligence artificielle

Que sont les intégrations ? Vecteurs et similarité cosinus expliqués simplement

Que sont les intégrations ? Un guide en langage simple sur les vecteurs, la similarité cosinus et la recherche sémantique, avec un petit exemple que…

Lire l'article →
DevOps

Déploiement Bleu-Vert vs Canary : différences, compromis et comment choisir

Explication du déploiement bleu-vert ou canari : comment chacun fonctionne, coût, risque et restauration, regroupement d'utilisateurs stable que nou…

Lire l'article →
Développement logiciel

Git merge vs rebase : différences, exemples et quand utiliser chacun

Git merge vs rebase expliqué avec de vraies sorties de log, un tableau comparatif, la règle d'or du rebase et un flux de travail sûr pour les équipes.

Lire l'article →
Développement logiciel

List comprehension Python : syntaxe, exemples et cas à éviter

La list comprehension Python expliquée : syntaxe, filtres, if-else, boucles imbriquées, temps mesurés et cas où une boucle for classique est préféra…

Lire l'article →
Bases de données et SQL

Jointures SQL expliquées avec exemples (Inner, Left, Right, Full)

Comprenez les jointures SQL pas à pas avec des tables et requêtes d'exemple : INNER, LEFT, RIGHT, FULL, CROSS et SELF, plus erreurs courantes et con…

Lire l'article →