La « boucle d'événement » de Node est au cœur de sa capacité à gérer une grande concurrence et un débit élevé. C'est la partie la plus magique, selon laquelle Node.js peut fondamentalement être compris comme "mono-thread", tout en permettant également de traiter des opérations arbitraires en arrière-plan. Cet article clarifiera le fonctionnement de la boucle d'événements afin que vous puissiez ressentir sa magie.
Programmation événementielle
Pour comprendre la boucle d'événements, vous devez d'abord comprendre la programmation pilotée par les événements. Il est apparu en 1960. De nos jours, la programmation événementielle est largement utilisée dans la programmation de l’interface utilisateur. L'une des principales utilisations de JavaScript est d'interagir avec le DOM, il est donc naturel d'utiliser une API basée sur les événements.
Défini simplement : la programmation basée sur les événements contrôle le flux d'une application à travers des événements ou des changements d'état. Généralement mis en œuvre via la surveillance d'événements, une fois l'événement détecté (c'est-à-dire que l'état change), la fonction de rappel correspondante est appelée. Cela vous semble familier ? En fait, c'est le principe de fonctionnement de base de la boucle d'événements Node.js.
Si vous êtes familier avec le développement JavaScript côté client, pensez aux méthodes .on*(), telles que element.onclick(), qui sont utilisées pour se combiner avec des éléments DOM pour offrir une interaction utilisateur. Ce mode de fonctionnement permet de déclencher plusieurs événements sur une seule instance. Node.js déclenche ce modèle via des EventEmitters (générateurs d'événements), comme dans les modules Socket et "http" côté serveur. Un ou plusieurs changements d'état peuvent être déclenchés à partir d'une seule instance.
Un autre schéma courant consiste à exprimer le succès et l’échec. Il existe généralement deux méthodes de mise en œuvre courantes. La première consiste à transmettre « l'exception d'erreur » dans le rappel, généralement comme premier paramètre de la fonction de rappel. Le second utilise le modèle de conception Promises et a ajouté ES6. Remarque* Le mode Promesse utilise une méthode d'écriture de chaîne de fonctions similaire à jQuery pour éviter une imbrication profonde des fonctions de rappel, telles que :
Le module "fs" (système de fichiers) adopte principalement le style de transmission des exceptions dans les rappels. Déclenchant techniquement certains appels, comme l'événement attaché fs.readFile(), mais l'API sert simplement à alerter l'utilisateur et à exprimer le succès ou l'échec de l'opération. Le choix d'une telle API repose sur des considérations architecturales plutôt que sur des limitations techniques.
Une idée fausse courante est que les émetteurs d'événements sont également intrinsèquement asynchrones lors du déclenchement d'événements, mais c'est incorrect. Vous trouverez ci-dessous un simple extrait de code pour le démontrer.
MyEmitter.prototype.doStuff = function doStuff() {
console.log('avant')
émetteur.emit('feu')
console.log('after')}
};
var me = new MyEmitter();
moi.on('fire', function() {
console.log('emit Fire');
});
me.doStuff();
// Sortie :
// avant
// émet tiré
// après
Remarque* Si émetteur.emit est asynchrone, la sortie doit être
// avant
// après
// émet tiré
Aperçu du mécanisme et pool de threads
Node lui-même repose sur plusieurs bibliothèques. L'un d'eux est libuv, l'étonnante bibliothèque permettant de gérer les files d'attente et l'exécution d'événements asynchrones.
Node utilise autant que possible le noyau du système d'exploitation pour implémenter les fonctions existantes. Comme générer des demandes de réponse, transférer les connexions et les confier au système pour traitement. Par exemple, les connexions entrantes sont mises en file d'attente via le système d'exploitation jusqu'à ce qu'elles puissent être gérées par Node.
Vous avez peut-être entendu dire que Node dispose d'un pool de threads, et vous vous demandez peut-être : "Si Node traite les tâches dans l'ordre, pourquoi avons-nous besoin d'un pool de threads ?" commande exécutée de manière asynchrone. Dans ce cas, Node.JS doit être capable de verrouiller le thread pendant un certain temps pendant son fonctionnement afin qu'il puisse continuer à exécuter la boucle d'événements sans être bloqué.
Ce qui suit est un exemple de schéma simple pour montrer son mécanisme de fonctionnement interne :
┌──────────────────────┐
╭──►│ minuteries minuteries
│ └───────────┬───────────┘
│ ┌───────────┴───────────┐
│ rappels en attente
│ └──────────┬───────────┘
|
│ │ │ SONDAGE ││── Connexions, │
│ depuis
│ ┌───────────┴───────────┐
╰─── ┤ setImmédiat
└───────────────────────┘
Il y a certaines choses difficiles à comprendre concernant le fonctionnement interne de la boucle événementielle :
Tous les rappels seront prédéfinis via process.nextTick() à la fin d'une étape de la boucle d'événement (par exemple, une minuterie) et avant de passer à l'étape suivante. Cela évitera les appels récursifs potentiels à process.nextTick(), provoquant une boucle infinie.
Les « rappels en attente » sont des rappels dans la file d'attente de rappel qui ne seront traités par aucun autre cycle de boucle d'événement (par exemple, transmis à fs.write).
Simplifiez l'interaction avec la boucle d'événements en créant un EventEmitter. Il s'agit d'un wrapper générique qui vous permet de créer plus facilement des API basées sur des événements. La façon dont les deux interagissent laisse souvent les développeurs confus.
L'exemple suivant montre qu'oublier qu'un événement est déclenché de manière synchrone peut entraîner la perte de l'événement.
fonction MyThing() {
EventEmitter.call(this);
doFirstThing();
this.emit('thing1');
>
util.inherits(MyThing, EventEmitter);
var mt = new MyThing();
mt.on('thing1', function onThing1() {
// Désolé, cet événement n'arrivera jamais
});
fonction MyThing() {
EventEmitter.call(this);
doFirstThing();
setImmediate(emitThing1, this);
>
util.inherits(MyThing, EventEmitter);
fonction émetThing1(self) {
self.emit('thing1');
>
var mt = new MyThing();
mt.on('thing1', function onThing1() {
// Exécuté
});
La solution suivante fonctionnera également, mais au détriment de certaines performances :
doFirstThing();
// L'utilisation de Function#bind() perdra en performances
setImmediate(this.emit.bind(this, 'thing1'));
>
util.inherits(MyThing, EventEmitter);
// Déclenchez une erreur et gérez-la immédiatement (de manière synchrone)
var euh = doSecondThing();
si (euh) {
This.emit('erreur', 'Plus de mauvaises choses');
Retour ;
>
>
Conclusion
Cet article aborde brièvement le fonctionnement interne et les détails techniques de la boucle d'événements. Tout est bien pensé. Un autre article discutera de l'interaction de la boucle d'événements avec le noyau du système et montrera la magie du fonctionnement asynchrone de NodeJS.