Event Loop Node.js expliqué aux débutants
Vous avez probablement entendu que Node.js est mono-thread et utilise une boucle d'événements pour gérer des milliers de connexions simultanées. Mais que cela signifie-t-il réellement ? Si vous débutez avec Node.js, la boucle d'événements peut sembler une boîte noire mystérieuse. Cet article la décompose en langage clair, pour que vous puissiez écrire du code Node.js meilleur et plus rapide.
Pourquoi la boucle d'événements existe
Les serveurs web traditionnels (comme Apache avec PHP) créent un nouveau thread ou processus pour chaque requête. Cela fonctionne, mais ne passe pas bien à l'échelle lorsque vous avez de nombreuses connexions simultanées—chaque thread consomme de la mémoire et du CPU. Node.js adopte une approche différente : il exécute votre JavaScript dans un seul thread et utilise une boucle d'événements pour gérer les opérations d'E/S de manière asynchrone. Cela signifie que votre code n'attend pas qu'une requête de base de données ou une lecture de fichier se termine ; il continue d'exécuter d'autres tâches et revient lorsque le résultat est prêt.
Les composants principaux
Avant de plonger dans la boucle d'événements, clarifions les acteurs clés :
- Pile d'appels (Call Stack) : Là où votre code JavaScript synchrone s'exécute. Les fonctions sont empilées et dépilées au fur et à mesure de leur exécution.
- API Node : Des API C++ qui gèrent des opérations coûteuses comme les E/S de fichiers, les requêtes réseau et les timers. Elles s'exécutent en arrière-plan (en utilisant le pool de threads libuv).
- File d'attente des callbacks : Là où les callbacks des opérations API Node terminées attendent d'être exécutés.
- Boucle d'événements : L'orchestrateur qui vérifie continuellement si la pile d'appels est vide et, si c'est le cas, déplace les callbacks de la file d'attente vers la pile.
Comment fonctionne la boucle d'événements : un exemple simple
Considérez ce code :
console.log('Start');
setTimeout(() => {
console.log('Timeout');
}, 0);
console.log('End');
Quelle est la sortie ? Si vous avez deviné Start, End, Timeout, vous avez raison. Même si le délai est de 0 milliseconde, le callback n'est pas exécuté immédiatement. Voici ce qui se passe :
console.log('Start')est empilé dans la pile d'appels, exécuté, puis dépilé.setTimeoutest appelé. Node.js enregistre le timer et le configure pour expirer après 0ms. Le callback est stocké, et la fonction retourne.console.log('End')est empilé, exécuté, puis dépilé.- Maintenant la pile d'appels est vide. La boucle d'événements vérifie la phase des timers et voit que le timer a expiré. Elle déplace le callback vers la pile d'appels, qui affiche
Timeout.
Cela démontre la nature non bloquante : le timer ne bloque pas le reste du code.
Les phases de la boucle d'événements
La boucle d'événements traite les callbacks en plusieurs phases, chacune ayant un objectif spécifique. Comprendre ces phases vous aide à prédire l'ordre d'exécution.
| Phase | Description |
|---|---|
| Timers | Exécute les callbacks planifiés par setTimeout() et setInterval(). |
| Pending Callbacks | Exécute les callbacks d'E/S différés à l'itération suivante de la boucle. |
| Idle, Prepare | Utilisé en interne par Node.js. |
| Poll | Récupère les nouveaux événements d'E/S ; exécute les callbacks liés aux E/S (presque tous sauf les timers, setImmediate et les callbacks de fermeture). |
| Check | Exécute les callbacks planifiés par setImmediate(). |
| Close Callbacks | Exécute les callbacks de fermeture, par exemple socket.on('close', ...). |
Entre chaque phase, Node.js vérifie les microtâches : les callbacks process.nextTick() et les Promesses. Celles-ci ont une priorité plus élevée et sont exécutées immédiatement après la fin de l'opération en cours, avant de passer à la phase suivante.
Microtâches : nextTick et Promesses
Les microtâches ne font pas partie des phases de la boucle d'événements ; elles sont traitées après chaque phase et après chaque callback. Cela les fait s'exécuter avant les timers et les callbacks d'E/S. Par exemple :
setTimeout(() => console.log('Timeout'), 0);
Promise.resolve().then(() => console.log('Promise'));
process.nextTick(() => console.log('nextTick'));
console.log('Sync');
Sortie : Sync, nextTick, Promise, Timeout. Le code synchrone s'exécute en premier, puis les microtâches (nextTick avant les Promesses), puis le timer.
Attention : Les appels récursifs à process.nextTick() peuvent affamer la boucle d'événements, empêchant les E/S de se produire. Utilisez setImmediate() pour les opérations récursives.
setImmediate vs setTimeout
setImmediate() est conçu pour exécuter un callback immédiatement après la fin de la phase de poll en cours. En revanche, setTimeout() avec 0ms attend la prochaine phase de timers. L'ordre entre eux peut varier selon le contexte, mais à l'intérieur d'un callback d'E/S, setImmediate() s'exécute toujours en premier.
Pourquoi cela compte pour votre code
Comprendre la boucle d'événements vous aide à éviter les pièges courants :
- Ne bloquez pas la boucle d'événements : Les opérations synchrones comme
fs.readFileSyncou les calculs lourds bloquent toute la boucle, rendant votre application non réactive. Utilisez les versions asynchrones ou déportez vers des threads de travail. - Utilisez les microtâches avec discernement : Elles s'exécutent avant les E/S, donc un traitement lourd de microtâches peut retarder les callbacks d'E/S.
- Comprenez l'ordre des callbacks : Lorsque vous mélangez timers, Promesses et E/S, connaissez les phases pour prédire la sortie.
Exemple pratique : lecture de fichier non bloquante
Voici comment la boucle d'événements permet des E/S non bloquantes :
const fs = require('fs');
console.log('Before read');
fs.readFile('large-file.txt', 'utf8', (err, data) => {
if (err) throw err;
console.log('File read complete');
});
console.log('After read');
Sortie : Before read, After read, puis File read complete. La lecture du fichier se fait en arrière-plan, et le callback est mis en file d'attente une fois terminé. Pendant ce temps, d'autres codes peuvent s'exécuter.
FAQ
Node.js est-il vraiment mono-thread ?
Node.js exécute votre JavaScript dans un seul thread, mais il utilise plusieurs threads dans le pool de threads libuv pour les E/S de fichiers, le DNS et d'autres opérations. Donc ce n'est pas entièrement mono-thread sous le capot.
Quelle est la différence entre la pile d'appels et la boucle d'événements ?
La pile d'appels exécute le code synchrone. La boucle d'événements gère les callbacks asynchrones, les déplaçant des files d'attente vers la pile d'appels lorsqu'elle est vide.
Puis-je créer plusieurs boucles d'événements ?
Non, chaque processus Node.js a une seule boucle d'événements. Cependant, vous pouvez utiliser des threads de travail pour exécuter des threads JavaScript séparés, chacun avec sa propre boucle d'événements.
Si vous travaillez avec des logs de serveur pour déboguer des problèmes de performance, essayez notre Analyseur de logs Nginx pour analyser et visualiser rapidement les données de logs.