En Node.js, quand on veut qu'une action se réalise à la suite d'une autre, on peut opter pour l'imbrication de callbacks ou l'enchaînement de promesses. Mais ces deux techniques lient étroitement l'action initiale et l'action consécutive, rendant plus compliquée toute modification ultérieure de l'action consécutive.
Les émetteurs d'événements proposent une autre manière de structurer cette relation : le modèle de publication et souscription. Dans ce modèle, un éditeur (l'émetteur d'événements) transmet un message (un événement), et un souscripteur reçoit l'événement et effectue une action.
L'avantage de ce modèle est que l'éditeur n'a pas besoin de connaître les souscripteurs. Un éditeur émet un message, et c'est à chaque souscripteur de réagir à ce message de la manière qui lui convient le mieux. Si l'on souhaite modifier le comportement de notre application, il est possible de changer la façon dont les souscripteurs réagissent aux événements, sans avoir à modifier l'éditeur.
Nous allons illustrer cette notion d'événementiel avec un peu de courrier ✉️
Créer un premier événement
Le module events
va nous permettre de créer et de traiter les événements. Dans ce module se trouve la classe EventEmitter
. Des objets de cette classe vont pouvoir émettre des événements avec la méthode emit()
.
const { EventEmitter } = require("events");
const ourEmitter = new EventEmitter();
ourEmitter.emit('hello');
Nous allons créer une classe Mailbox
, qui hérite de l'EventEmitter
, pour représenter notre boîte aux lettres. À cette boîte aux lettres, nous ajoutons une taille maximale. Une fois cette limite atteinte, nous considérerons qu'elle est pleine et qu'elle ne peut plus recevoir de courrier supplémentaire.
Ajoutons dans la classe une méthode receiveMail()
qui va envoyer un événement quand un nouveau message arrive dans la boîte. Une fois la boîte pleine, nous arrêtons d'insérer de nouveaux messages mais nous écrivons un log.
Traiter les événements
Notre boîte aux lettres est capable d'émettre des événements mais malheureusement, personne ne les écoute actuellement. Pour y remédier, ajoutons un souscripteur avec la méthode on()
.
La fonction on
prend en paramètre le nom de l'événement qu'on souhaite écouter et une fonction callback qui va traiter l'événement. Dans notre cas, on souhaite écouter l'évènement mail
émis par notre boîte aux lettres et lorsqu'un nouveau courrier arrive, on log un "You've got mail!"
.
$ node mailbox.js
You've got mail!
Il existe également la fonctionaddListener()
pour ajouter un souscripteur. Les deux fonctions ont la même utilité, puisqueaddListener()
est en fait un alias de la méthodeon()
.
Comme notre boîte ne peut contenir que 3 messages, si l'on essaye d'envoyer plus de 3 messages, la boîte arrêtera d'envoyer des événements mail
et affichera dans le terminal un message de saturation.
const mailbox = new Mailbox(3);
mailbox.on("mail", () => console.log("You've got mail!"));
mailbox.receiveMail("hello"); // Affiche "You've got mail!"
mailbox.receiveMail("this"); // Affiche "You've got mail!"
mailbox.receiveMail("is"); // Affiche "You've got mail!"
mailbox.receiveMail("me"); // Affiche "Mailbox is full"
mailbox.receiveMail("!"); // Affiche "Mailbox is full"
Un autre souscripteur qui n'a pas l'envie d'être sollicité en permanence veut écouter la boîte aux lettres qu'une seule fois. Ainsi la méthode once()
permet d'écouter l'événement de façon unique avant de se désabonner automatiquement.
mailbox.on("mail", () => console.log("You've got mail!"));
mailbox.once("mail", () => console.log("One mail is all it takes..."));
mailbox.receiveMail("first mail");
mailbox.receiveMail("second mail");
mailbox.receiveMail("last mail");
// You've got mail!
// One mail is all it takes...
// You've got mail!
// You've got mail!
Comme vous l'avez compris, il y a bien deux souscripteurs pour le même événement. Mais il est tout à fait possible de rajouter plusieurs souscripteurs qui écoutent le même événement, et chaque souscripteur va lancer son callback quand l'événement sera émis.
Enrichir les messages
Pour le moment, notre boîte aux lettres envoie des événements vides, c'est-à-dire sans donnée supplémentaire. La méthode emit()
de la classe EventEmitter
accepte des paramètres supplémentaires, qui sont des éléments qui vont accompagner l'événement. Nous allons modifier la classe Mailbox
afin d'émettre des événements enrichis.
L'événement mail
est enrichi avec le texte du mail et la date de réception. Quand on lance le programme, on voit que le callback récupère les éléments envoyés par notre boîte aux lettres.
$ node mailbox.js
Mail received at 1683031214652! It says: hello
Mail received at 1683031214655! It says: this
Mail received at 1683031214655! It says: is
Mailbox is full
Mailbox is full
Gestions des erreurs
Modifions une nouvelle fois le fichier. Au lieu d'écrire dans la console, nous allons émettre un événement error
qui contient une erreur.
receiveMail(email) {
if (this.box.length < this.maxSize) {
this.box.push(email);
this.emit("mail", email, Date.now());
return;
}
this.emit("error", new Error("Mailbox is full"));
}
Personne n'écoute cet événement error
pour le moment mais exécutons quand même le fichier pour voir ce qu'il se passe.
$ node mailbox.js
Mail received at 1683031792230! It says: hello
Mail received at 1683031792233! It says: this
Mail received at 1683031792233! It says: is
node:events:491
throw er; // Unhandled 'error' event
^
Error: Mailbox is full
at Mailbox.receiveMail (mails.js:16:28)
at Object.<anonymous> (mails.js:25:9)
at Module._compile (node:internal/modules/cjs/loader:1254:14)
at Module._extensions..js (node:internal/modules/cjs/loader:1308:10)
at Module.load (node:internal/modules/cjs/loader:1117:32)
at Module._load (node:internal/modules/cjs/loader:958:12)
at Function.executeUserEntryPoint [as runMain] (node:internal/modules/run_main:81:12)
at node:internal/main/run_main_module:23:47
Emitted 'error' event on Mailbox instance at:
at Mailbox.receiveMail (mails.js:16:14)
at Object.<anonymous> (mails.js:25:9)
[... lines matching original stack trace ...]
at node:internal/main/run_main_module:23:47
Oups, la boîte aux lettres a fait exploser notre programme. Dans le message, on voit bien que l'événement error
a été émis par notre boîte aux lettres (Emitted 'error' event on Mailbox instance
) mais il n'a pas été géré (Unhandled 'error' event
).
Si un EventEmitter
émet un événement error
et qu'aucun souscripteur n'est disponible pour l'écouter, alors l'erreur est levée, la stack trace est imprimée et le processus Node.js se termine.
Il faut donc rajouter un souscripteur
qui va se charger des erreurs émises par notre boîte aux lettres.
$ node mailbox.js
Mail received at 1683033385024! It says: hello
Mail received at 1683033385027! It says: this
Mail received at 1683033385027! It says: is
Something went wrong with the mailbox: Error: Mailbox is full
Something went wrong with the mailbox: Error: Mailbox is full
Les événements dans le noyau Node.js
Étant donnée sa nature de moteur Javascript asynchrone, Node.js s'appuie lui-même sur les événements dans son propre noyau.
Dans ce fichier, on utilise le module fs
ainsi que la variable globale process
qui nous sont fournis clé en main par Node. La fonction createReadStream
du module fs
est importée et nous renvoie un flux de données lisant le fichier courant (__filename
).
Les variables readFileStream
et process
écoutent des événements prédéfinis dans le cœur de Node.js, il n'y a pas de emit(data)
que nous avons codé dans ce fichier. Comme avec notre boîte aux lettres, on écoute des événements, et on les traite.
Le flux nous signale quand il est prêt à être utilisé avec un événement ready
. Quand il obtient de la donnée, il envoie un événement data
avec la donnée. Enfin, quand il n'y a plus de donnée à traiter, le flux émet un événement end
.
Quant au process
, il envoie un événement exit
quand Node est sur le point de s'arrêter. On décide de l'écouter, et on affiche le code de sortie du processus (0 si tout s'est bien passé, un nombre non nul en cas d'échec).
Si on exécute ce code, on voit les différents événements émis nativement au fil du temps.
$ node my-events.js
Ready to read file
100 bytes read
100 bytes read
100 bytes read
100 bytes read
100 bytes read
48 bytes read
Finished reading file, 548 bytes read in total
Exiting Node, status code = 0
Pour aller plus loin
Approfondissez vos connaissances sur le module events
avec la documentation officielle de Node.js.
Ensuite, pourquoi ne pas explorer d'autres mécanismes d'événements, cette fois-ci côté navigateur ? Découvrez comment le Local Storage utilise les événements en JavaScript pour créer des interactions dynamiques.