2.2EventEmitter & programmation événementielle
L'essentiel en 30 secondes
- Émettre puis écouter : .on(enregistrement) déclare la réaction, .emit(déclenchement) l'appelle — les deux côtés ne se connaissent pas.
- Partout sous le capot : serveur HTTP, streams, sockets sont des EventEmitters — savoir écouter, c'est savoir utiliser Node.
- error ne pardonne pas : un événement error émis sans écouteur fait planter le programme — toujours écouter les erreurs.
Mini-cas filé — Salim signale la progression de l'import
Salim fait évoluer son inventaire (2.1) : l'import des rapports prend du temps, et l'équipe veut suivre la progression sans appeler son script en boucle. Il crée un émetteur qui publie un événement pour chaque fichier traité et un événement de fin — l'afficheur de progression et le journal écoutent indépendamment, sans que l'import les connaisse. En 2.3, ce sont les streams eux-mêmes qui émettront les événements data/end, et en 2.4 le serveur HTTP émettra une requête à chaque visite.
Suite du fil rouge 2.1 : l'inventaire de Salim devient découplé par événements — et continue en 2.3 et 2.4.
1. Émettre des événements, écouter et réagir
Node est, dans son essence, événementiel (rappel du niveau 0 : la boucle d'événements). La programmation événementielle repose sur une idée simple : au lieu que le code appelle directement d'autres fonctions, des objets émettent des événements (« il s'est passé quelque chose ») et d'autres parties du code les écoutent et y réagissent. C'est un mécanisme de découplage : l'émetteur ne sait pas qui l'écoute, l'écouteur ne sait pas d'où vient l'événement — ils communiquent par le nom de l'événement. L'import de Salim n'a plus à connaître l'afficheur : il émet, l'afficheur écoute.
2. .on et .emit, plus once et off
Le fonctionnement tient en deux méthodes. .on(nom, callback) : ENREGISTRER un écouteur — « quand X est émis, exécute cette fonction ». .emit(nom, ...données) : ÉMETTRE — « déclenche X avec ces données », tous ses écouteurs sont alors appelés. On crée son émetteur avec new EventEmitter() du module natif events (ou en faisant hériter une classe). Utiles aussi : .once() (écouter UNE seule fois, désabonnement automatique), .off() / .removeListener() (arrêter d'écouter). Et règle d'or : l'événement error est spécial — émis sans écouteur, il fait planter le programme. Salim écoute donc toujours les erreurs de son import.
3. Le modèle sous toutes les API Node
Pourquoi c'est important : c'est le modèle sous-jacent de nombreuses API — serveurs HTTP (événement à chaque requête), streams ('data', 'end'), sockets, et beaucoup de bibliothèques exposent leurs comportements via des événements. Savoir écouter (.on('data', ...), .on('request', ...)) est constant en Node. L'EventEmitter permet aussi une architecture découplée et extensible : ajouter un comportement = ajouter un écouteur, sans toucher à l'émetteur. On ne créera pas forcément beaucoup d'émetteurs personnalisés au début, mais on les RENCONTRERA constamment — d'où l'importance du modèle émettre/écouter.
🔎 Exemple concret — 🔎 Exemple concret — La progression de l'import de Salim
Un émetteur, deux écouteurs indépendants, comme chez Salim :
const { EventEmitter } = require('node:events');
const progression = new EventEmitter();
progression.on('fichier', (nom) => console.log('Traité :', nom));
progression.on('termine', (total) => console.log('Import fini :', total));
progression.on('error', (err) => console.error('Échec import :', err.message));
progression.emit('fichier', 'rapport-01.txt');
progression.emit('termine', 1);
Lecture : l'import émet, l'affichage et le journal écoutent sans se connaître.
Leçon : l'écouteur error est obligatoire — sinon, plantage.
Vocabulaire de la section
- Programmation événementielle
- Modèle où des objets émettent des événements et d'autres parties du code y réagissent en écoutant ; découplage par le nom de l'événement.
Exemple simple : L'import de Salim émet, la progression et le journal écoutent.
Ce que ce n'est pas : un appel direct de fonction où l'appelant connaît le destinataire.
Réflexe : plusieurs réactions indépendantes = penser événements.
- EventEmitter
- Classe native (module events) au cœur du modèle événementiel de Node ; de nombreux objets natifs (serveur HTTP, streams) en héritent.
Exemple simple : Salim instancie new EventEmitter() pour sa progression d'import.
Ce que ce n'est pas : une Promise, qui ne se résout qu'une seule fois.
Réflexe : objet Node qui émet = probablement un EventEmitter.
- .on() / .emit()
- on(événement, callback) enregistre un écouteur ; emit(événement, données) déclenche l'événement et appelle tous ses écouteurs.
Exemple simple : progression.on('fichier', ...) puis progression.emit('fichier', nom).
Ce que ce n'est pas : un return de fonction qui rend une valeur à l'appelant.
Réflexe : .on avant .emit, sinon l'événement part dans le vide.
- .once() / .off()
- once() écoute un événement une seule fois (désabonnement auto) ; off()/removeListener() arrête d'écouter.
Exemple simple : Salim écoute l'événement de fin d'import avec .once().
Ce que ce n'est pas : un écouteur qui reste actif pour toujours par défaut.
Réflexe : événement unique attendu = .once().
- Événement error
- Événement spécial : un émetteur qui émet error sans écouteur fait planter le programme — toujours écouter les erreurs.
Exemple simple : Salim ajoute progression.on('error', ...) dès la création.
Ce que ce n'est pas : un événement ordinaire qu'on peut ignorer sans risque.
Réflexe : nouvel émetteur = écouteur error immédiat.
En pratique — En pratique — Utiliser la programmation événementielle
- Créez un EventEmitter (const emitter = new EventEmitter()), enregistrez un écouteur avec .on('event', callback) et émettez avec .emit('event', données).
- Passez des données lors de l'émission et récupérez-les dans l'écouteur ; ajoutez plusieurs écouteurs sur le même événement.
- Testez .once() (écoute unique) et écoutez toujours l'événement 'error' (sinon un plantage).
- Repérez que des objets Node natifs sont des EventEmitters : écoutez par exemple les événements d'un serveur HTTP ou d'un stream (.on('data', ...)).
Script prêt à dire — expliquer le découplage à un collègue (30 s)
« Mon import ne connaît ni l'afficheur ni le journal : il émet juste un événement par fichier traité. Chacun s'abonne avec .on et réagit à sa façon. Si demain on ajoute une alerte e-mail, on ajoute un écouteur — sans toucher une ligne de l'import. »
À dire en montrant l'ajout d'un écouteur en direct ; c'est la démonstration qui convainc.
Modèle copier-coller — import suivi par événements
const { EventEmitter } = require('node:events');
class Importeur extends EventEmitter {
async importer(fichiers) {
let traites = 0;
for (const f of fichiers) {
try {
traites += 1;
this.emit('fichier', f, traites, fichiers.length);
} catch (err) {
this.emit('error', err);
return;
}
}
this.emit('termine', traites);
}
}
const importeur = new Importeur();
importeur.on('fichier', (f, n, total) => console.log(n + '/' + total + ' : ' + f));
importeur.once('termine', (total) => console.log('Import fini : ' + total + ' fichiers'));
importeur.on('error', (err) => console.error('Échec : ' + err.message));
importeur.importer(['rapport-01.txt', 'rapport-02.txt']);
Script solo
Un seul écouteur de progression suffit : l'émetteur reste identique, on ajoute des écouteurs quand le besoin grandit.
Équipe à Alger
Salim ajoute un écouteur qui journalise chaque fichier dans le fichier de suivi partagé — sans modifier l'import.
Événement unique
Fin d'import, initialisation terminée : .once() évite les doubles réactions si l'événement est réémis.
Objet natif
Plutôt que créer un émetteur, écouter celui qui existe : serveur HTTP, stream, process — .on('data'/'request'/'exit').
✓ Bonnes pratiques
- Enregistrez les écouteurs (.on) avant d'émettre, sinon l'événement part dans le vide.
- Écoutez toujours error sur chaque émetteur (sinon, plantage du programme).
- Ajoutez un comportement en ajoutant un écouteur, pas en modifiant l'émetteur.
✗ Pièges fréquents
- Utiliser une Promise pour un flux répété (requêtes, morceaux de stream).
- Oublier l'écouteur error (plantage au premier problème).
- Émettre avant d'avoir enregistré les écouteurs (événements perdus).
Points clés à retenir
- La PROGRAMMATION ÉVÉNEMENTIELLE (au cœur de Node) : des objets ÉMETTENT des événements, d'autres les ÉCOUTENT et réagissent — découplage par le nom de l'événement.
- L'EVENTEMITTER (module events) est la classe centrale ; de nombreux objets natifs en héritent (serveur HTTP, streams, sockets).
- Deux méthodes clés : .on(événement, callback) (écouter) et .emit(événement, données) (déclencher). Aussi : .once() (une fois), .off() (arrêter).
- Toujours écouter l'événement 'error' (émis sans écouteur = plantage). Comprendre l'EventEmitter = comprendre comment interagir avec les API Node.
Défi 48h — appliquez demain
Transformez un traitement en boucle de votre code en émetteur : un événement par élément traité, un événement de fin en .once(), et un écouteur error. Ajoutez ensuite un second écouteur (journal, compteur) sans toucher à l'émetteur.
- Je peux expliquer émettre/écouter et le découplage par le nom.
- Je sais utiliser .on/.emit/.once/.off et écouter error.
- J'ai découplé un traitement réel avec deux écouteurs indépendants.
Questions fréquentes
Pourquoi la programmation événementielle est-elle si présente en Node, et où la rencontre-t-on ?
Parce qu'elle épouse l'architecture asynchrone non-bloquante de Node : requêtes qui arrivent, données réseau qui se présentent, lectures qui se terminent — tout cela survient à des moments imprévisibles, et la réaction naturelle est « quand X arrive, fais ceci ». On la rencontre partout : le serveur HTTP émet à chaque requête, les streams émettent 'data'/'end'/'error' (section 2-3), les sockets signalent connexion et fermeture, process émet 'exit', et les WebSockets (section 4-3) vivent d'événements. Comme Salim, on passe son temps à écouter — d'où l'importance du modèle.
Quand créer mon propre EventEmitter plutôt que d'utiliser callbacks ou Promises ?
Quand l'événement peut se produire plusieurs fois et que le découplage compte. Une Promise se résout une seule fois : parfaite pour « fais cette opération et rends-moi son résultat » (lire un fichier, interroger une base). Un EventEmitter émet autant de fois que nécessaire vers zéro, un ou plusieurs écouteurs indépendants : chaque requête d'un serveur, chaque morceau d'un stream, chaque fichier de l'import de Salim. Règle : un résultat unique en relation directe = Promise/async-await ; flux répété et consommateurs découplés = EventEmitter.
Salim veut réagir à chaque fichier importé, autant de fois que nécessaire, avec deux affichages indépendants. Quel outil ?