Zum Inhalt springen
aviral gupta

// B5.1 · ca. 31 Min. · Einstieg

Der EventEmitter

Nach dieser Lektion können Sie einen EventEmitter anlegen und erweitern, Listener hinzufügen und entfernen, vorhersagen, wann sie laufen, das 'error'-Event behandeln und mit einem Promise auf ein Event warten.

Lektion 1 von 5 in B5 Ereignisse, Eingaben und erste Tests

Anfang des Moduls

Danach können Sie

  • Listener mit on und once hinzufügen, Events mit Argumenten emittieren und Listener mit off entfernen
  • Vorhersagen, wann Listener laufen und was emit liefert, und das 'error'-Event behandeln
  • EventEmitter in einer Klasse erweitern, events.once() abwarten und Listener nicht anwachsen lassen
  1. Aufwärmen · Aufgabe 1 von 7

    Aufwärmen aus B4.2: Sie haben process.on('unhandledRejection', listener) geschrieben. Was gibt dieses Programm aus?

    import {EventEmitter} from 'node:events';
    
    console.log(process instanceof EventEmitter, typeof process.on);
  2. Vorhersagen · Aufgabe 2 von 7

    Sagen Sie es voraus, bevor Sie weiterlesen. In welcher Reihenfolge werden die Zeilen ausgegeben?

    import {EventEmitter} from 'node:events';
    
    const door = new EventEmitter();
    door.on('open', () => console.log('listener 1'));
    door.on('open', () => console.log('listener 2'));
    console.log('before emit');
    door.emit('open');
    console.log('after emit');
  3. Üben · Aufgabe 3 von 7

    door emittiert open zweimal. Ergänzen Sie die Methode, damit first visitor nur beim ersten Mal ausgegeben wird.

    door.____('open', () => console.log('first visitor'));
    door.('open', () => console.log('first visitor'));
  4. Üben · Aufgabe 4 von 7

    Ordnen Sie jeder EventEmitter-Methode zu, was sie tut.

  5. Üben · Aufgabe 5 von 7

    Was gibt dieses Programm aus?

    import {EventEmitter} from 'node:events';
    
    const shop = new EventEmitter();
    shop.on('sale', (item, price) => console.log(item, price));
    console.log(shop.emit('sale', 'tea', 3));
    console.log(shop.emit('refund', 'tea'));
  6. Denksport · Aufgabe 6 von 7

    Knobelaufgabe. Dieselbe Funktion count wird zweimal an einen EventEmitter und zweimal an ein EventTarget gehängt, das globale Objekt, das auch Browser haben. Beide bekommen dann ein Event. Was gibt das Programm aus?

    import {EventEmitter} from 'node:events';
    
    let calls = 0;
    const count = () => calls++;
    
    const emitter = new EventEmitter();
    emitter.on('ping', count);
    emitter.on('ping', count);
    emitter.emit('ping');
    
    const target = new EventTarget();
    target.addEventListener('ping', count);
    target.addEventListener('ping', count);
    target.dispatchEvent(new Event('ping'));
    
    console.log(calls);
  7. Anwenden · Aufgabe 7 von 7

    Mini-Aufgabe. Schreiben Sie eine Klasse Countdown, die EventEmitter erweitert. Ihre Methode start(from, ms) emittiert alle ms Millisekunden 'tick' mit der verbleibenden Zahl, von from abwärts bis 1, stoppt dann ihr Intervall und emittiert 'end'. Geben Sie in derselben Datei jeden tick aus, warten Sie mit once aus node:events auf das Ende und geben Sie lift-off aus. Starten Sie mit node main.js.

    Prüfen Sie Ihr Ergebnis anhand dieser Liste

Selbst programmieren

Lesen Sie das ausgearbeitete Beispiel und lösen Sie dann die Übungen. Ihr Code läuft in Ihrem Browser oder auf Ihrem Computer und wird nie hochgeladen.

Ausgearbeitetes Beispiel

Ein Job, der seinen Fortschritt meldet

Job erweitert EventEmitter und emittiert nach jedem Schritt progress, dann done, oder error, wenn er scheitert. Das Programm hört mit on und once zu, wartet mit once aus node:events auf done, behandelt das error-Event eines kaputten Jobs und entfernt zum Schluss einen Listener mit off. Achten Sie darauf, welche Listener übrig bleiben und was emit vor und nach off liefert.

main.js

import {EventEmitter, once} from 'node:events';

// A job that reports its progress as events.
class Job extends EventEmitter {
  constructor(name, steps) {
    super(); // sets up the emitter; must come before this
    this.name = name;
    this.steps = steps;
  }

  async run() {
    for (let step = 1; step <= this.steps; step++) {
      await new Promise((resolve) => setTimeout(resolve, 10));
      this.emit('progress', step, this.steps);
    }
    if (this.name === 'broken') this.emit('error', new Error(this.name + ' failed'));
    else this.emit('done', this.name);
  }
}

const backup = new Job('backup', 3);
backup.on('progress', (step, total) => console.log('progress ' + step + '/' + total));
backup.once('progress', () => console.log('(only after the first progress event)'));
backup.run();
const [name] = await once(backup, 'done'); // a promise for the next 'done'
console.log('done:', name);
console.log('progress listeners left:', backup.listenerCount('progress'));

const broken = new Job('broken', 1);
broken.on('error', (error) => console.log('error event:', error.message));
await broken.run();

// Remove a listener you no longer need, with the same function.
const bus = new EventEmitter();
const onStop = () => console.log('stopping');
bus.on('stop', onStop);
console.log('emit returns', bus.emit('stop'));
bus.off('stop', onStop);
console.log('emit returns', bus.emit('stop'));

Ausführen mit

node main.js

Ausgabe

progress 1/3
(only after the first progress event)
progress 2/3
progress 3/3
done: backup
progress listeners left: 1
error event: broken failed
stopping
emit returns true
emit returns false
  • Der once-Listener lief nur nach der ersten progress-Zeile und wurde entfernt: Ein progress-Listener bleibt.
  • await once(backup, 'done') lieferte das Array der Argumente von done; [name] nahm das erste.
  • Der kaputte Job emittierte error, und weil es einen Listener gab, lief das Programm weiter.
  • Nach off fand emit keinen Listener und lieferte false.

Übungen

Übung 1 von 2

Ein Mini-Emitter

node:events gibt es nur in Node.js, also schreiben Sie seinen Kern selbst; er läuft auch im Browser. createEmitter() liefert {on, once, off, emit}. emit(name, ...args) ruft jeden Listener von name mit args auf, in der Reihenfolge des Hinzufügens, und liefert true, wenn es welche gab, sonst false. off entfernt eine Instanz der Funktion. Der Listener von once läuft nur beim nächsten emit. Ein Listener, der sich während emit selbst entfernt, darf nicht dazu führen, dass der nächste übersprungen wird.

Tab rückt ein, Umschalt+Tab rückt aus. Um den Editor mit der Tastatur zu verlassen, drücken Sie Esc und dann Tab.

Beim ersten Ausführen lädt Ihr Browser den JavaScript-Runner herunter (bis zu 0.1 MB) und speichert ihn im Cache. Ihr Code läuft in der eigenen Engine Ihres Browsers und bleibt auf Ihrem Gerät.

Hinweise
  1. Hinweis 1

    Finden Sie die Funktion in off mit list.lastIndexOf(fn) und entfernen Sie sie mit list.splice(index, 1), nur wenn der Index nicht -1 ist.

  2. Hinweis 2

    Gehen Sie in emit über eine Kopie, [...(listeners.get(name) ?? [])], rufen Sie fn(...args) auf und liefern Sie copy.length > 0.

  3. Hinweis 3

    Fügen Sie für once mit on einen Wrapper (...args) => { off(name, wrapper); fn(...args); } hinzu, der sich zuerst selbst entfernt.

Eine Lösung zeigen

Ein möglicher Lösungsweg. Ihrer kann anders aussehen und trotzdem alle Prüfungen bestehen.

// A small emitter with the rules of EventEmitter's on, once, off and emit.
export function createEmitter() {
  const listeners = new Map(); // event name -> array of functions

  function on(name, fn) {
    if (!listeners.has(name)) listeners.set(name, []);
    listeners.get(name).push(fn);
  }

  function off(name, fn) {
    const list = listeners.get(name) ?? [];
    const index = list.lastIndexOf(fn); // the most recently added instance
    if (index !== -1) list.splice(index, 1);
  }

  function once(name, fn) {
    const wrapper = (...args) => {
      off(name, wrapper);
      fn(...args);
    };
    on(name, wrapper);
  }

  function emit(name, ...args) {
    const list = [...(listeners.get(name) ?? [])]; // a copy: changes wait for the next emit
    for (const fn of list) fn(...args);
    return list.length > 0;
  }

  return {on, once, off, emit};
}

const door = createEmitter();
door.on('open', (who) => console.log('hello', who));
door.once('open', () => console.log('first visitor'));
console.log(door.emit('open', 'Ada'), door.emit('open', 'Lin'), door.emit('close'));
Auf dem eigenen Computer ausführen

Installieren Sie Node.js 24 LTS oder neuer. Speichern Sie diese Dateien in einem Ordner, öffnen Sie dort ein Terminal und führen Sie die Befehle unten aus.

main.js

// A small emitter with the rules of EventEmitter's on, once, off and emit.
export function createEmitter() {
  const listeners = new Map(); // event name -> array of functions

  function on(name, fn) {
    if (!listeners.has(name)) listeners.set(name, []);
    listeners.get(name).push(fn);
  }

  function off(name, fn) {}

  function once(name, fn) {
    on(name, fn);
  }

  function emit(name, ...args) {
    for (const fn of listeners.get(name) ?? []) fn();
  }

  return {on, once, off, emit};
}

const door = createEmitter();
door.on('open', (who) => console.log('hello', who));
door.once('open', () => console.log('first visitor'));
console.log(door.emit('open', 'Ada'), door.emit('open', 'Lin'), door.emit('close'));

main.test.js

import {test} from 'node:test';
import assert from 'node:assert/strict';
import {createEmitter} from './main.js';

test('Listener laufen in der Reihenfolge des Hinzufügens, mit den Argumenten von emit', () => {
  const e = createEmitter();
  const seen = [];
  e.on('sale', (item, price) => seen.push('first ' + item + ' ' + price));
  e.on('sale', (item) => seen.push('second ' + item));
  e.emit('sale', 'tea', 3);
  assert.deepEqual(seen, ['first tea 3', 'second tea'], `die Listener sahen ${JSON.stringify(seen)}`);
});

test('emit liefert true mit Listenern und false ohne', () => {
  const e = createEmitter();
  e.on('sale', () => {});
  assert.equal(e.emit('sale'), true, 'emit für ein Event mit Listener sollte true liefern');
  assert.equal(e.emit('refund'), false, 'emit für ein Event ohne Listener sollte false liefern');
});

test('off entfernt eine Instanz der Funktion', () => {
  const e = createEmitter();
  let calls = 0;
  const count = () => calls++;
  e.on('ping', count);
  e.on('ping', count);
  e.off('ping', count);
  e.emit('ping');
  assert.equal(calls, 1, `count lief nach zweimal on und einmal off ${calls}-mal; erwartet war 1-mal`);
  e.off('ping', count);
  assert.equal(e.emit('ping'), false, 'nach dem Entfernen beider Instanzen sollte emit false liefern');
});

test('Ein once-Listener läuft nur beim nächsten emit', () => {
  const e = createEmitter();
  let calls = 0;
  e.once('ring', (n) => (calls += n));
  assert.equal(e.emit('ring', 5), true, 'das erste emit sollte den once-Listener finden');
  assert.equal(e.emit('ring', 5), false, 'das zweite emit sollte keinen Listener finden');
  assert.equal(calls, 5, `der once-Listener kam auf ${calls}; erwartet war 5`);
});

test('Ein Listener, der sich selbst entfernt, lässt den nächsten nicht ausfallen', () => {
  const e = createEmitter();
  const seen = [];
  const first = () => {
    seen.push('first');
    e.off('open', first);
  };
  e.on('open', first);
  e.on('open', () => seen.push('second'));
  e.emit('open');
  assert.deepEqual(seen, ['first', 'second'], `gelaufen sind die Listener ${JSON.stringify(seen)}`);
});

package.json

{
  "type": "module"
}

package.json sagt Node.js, dass die .js-Dateien Module sind; lassen Sie die Datei im Ordner.

Programm ausführen:

node main.js

Prüfungen ausführen (learnrun.js muss im selben Ordner liegen):

node --test
learnrun.js herunterladen

Übung 2 von 2

Ein Wasserkocher, der Events emittiert

Kettle erweitert EventEmitter und beginnt bei 20 Grad. Schreiben Sie heat(target): Erhöhen Sie die Temperatur in Schritten von 20, nie über target hinaus, und emittieren Sie 'temperature' mit jedem neuen Wert; bei 100 emittieren Sie 'boiled'. Ein target über 100 emittiert 'error' mit einem RangeError und heizt nicht. Ohne error-Listener wird dieser Fehler geworfen. Starten Sie node main.js, dann node --test.

Diese Übung braucht Node.js auf Ihrem Computer (die Browser-Version kann sie nicht ausführen). Dateien und Befehle stehen unten.

Hinweise
  1. Hinweis 1

    Prüfen Sie zuerst target > 100: this.emit("error", new RangeError(...)), dann return.

  2. Hinweis 2

    Schleife while (this.temperature < target), setzen Sie this.temperature = Math.min(this.temperature + 20, target), dann this.emit("temperature", this.temperature).

  3. Hinweis 3

    Nach der Schleife: wenn this.temperature === 100, dann this.emit("boiled"). Ein emit("error") ohne Listener wirft von selbst; Sie schreiben kein throw.

Eine Lösung zeigen

Ein möglicher Lösungsweg. Ihrer kann anders aussehen und trotzdem alle Prüfungen bestehen.

import {EventEmitter} from 'node:events';

// A kettle that reports what it does as events.
export class Kettle extends EventEmitter {
  constructor() {
    super();
    this.temperature = 20;
  }

  // Heats in steps of 20 degrees up to target, with a 'temperature' event for each
  // new value, then 'boiled' at 100. Above 100: an 'error' event, and no heating.
  heat(target) {
    if (target > 100) {
      this.emit('error', new RangeError('cannot heat above 100, asked for ' + target));
      return;
    }
    while (this.temperature < target) {
      this.temperature = Math.min(this.temperature + 20, target);
      this.emit('temperature', this.temperature);
    }
    if (this.temperature === 100) this.emit('boiled');
  }
}

const kettle = new Kettle();
kettle.on('temperature', (degrees) => console.log(degrees + ' degrees'));
kettle.once('boiled', () => console.log('boiled'));
kettle.on('error', (error) => console.log('error:', error.message));
kettle.heat(100);
kettle.heat(120);
Auf dem eigenen Computer ausführen

Installieren Sie Node.js 24 LTS oder neuer. Speichern Sie diese Dateien in einem Ordner, öffnen Sie dort ein Terminal und führen Sie die Befehle unten aus.

main.js

import {EventEmitter} from 'node:events';

// A kettle that reports what it does as events.
export class Kettle extends EventEmitter {
  constructor() {
    super();
    this.temperature = 20;
  }

  // Heats in steps of 20 degrees up to target, with a 'temperature' event for each
  // new value, then 'boiled' at 100. Above 100: an 'error' event, and no heating.
  heat(target) {
    this.temperature = target;
  }
}

const kettle = new Kettle();
kettle.on('temperature', (degrees) => console.log(degrees + ' degrees'));
kettle.once('boiled', () => console.log('boiled'));
kettle.on('error', (error) => console.log('error:', error.message));
kettle.heat(100);
kettle.heat(120);

main.test.js

import {test} from 'node:test';
import assert from 'node:assert/strict';
import {Kettle} from './main.js';

test('heat(80) emittiert temperature 40, 60 und 80', () => {
  const kettle = new Kettle();
  const seen = [];
  kettle.on('temperature', (degrees) => seen.push(degrees));
  kettle.heat(80);
  assert.deepEqual(seen, [40, 60, 80], `die temperature-Events waren ${JSON.stringify(seen)}`);
});

test('Die Schritte gehen nie über das Ziel hinaus', () => {
  const kettle = new Kettle();
  const seen = [];
  kettle.on('temperature', (degrees) => seen.push(degrees));
  kettle.heat(50);
  assert.deepEqual(seen, [40, 50], `die temperature-Events waren ${JSON.stringify(seen)}`);
});

test('boiled wird einmal emittiert, wenn 100 erreicht ist', () => {
  const kettle = new Kettle();
  let boiled = 0;
  kettle.on('boiled', () => boiled++);
  kettle.heat(60);
  assert.equal(boiled, 0, 'bei 60 Grad sollte boiled nicht emittiert werden');
  kettle.heat(100);
  assert.equal(boiled, 1, `boiled wurde ${boiled}-mal emittiert; erwartet war 1-mal`);
});

test('Über 100 ein error-Event mit RangeError und kein Heizen', () => {
  const kettle = new Kettle();
  const errors = [];
  kettle.on('error', (error) => errors.push(error));
  kettle.heat(120);
  assert.equal(errors.length, 1, `es gab ${errors.length} error-Events; erwartet war 1`);
  assert.ok(errors[0] instanceof RangeError, 'das error-Event sollte einen RangeError tragen');
  assert.equal(kettle.temperature, 20, `die Temperatur ist ${kettle.temperature}; sie sollte 20 bleiben`);
});

test('Ohne error-Listener wirft heat(120) den RangeError', () => {
  assert.throws(() => new Kettle().heat(120), RangeError, 'heat(120) ohne error-Listener sollte einen RangeError werfen');
});

package.json

{
  "type": "module"
}

package.json sagt Node.js, dass die .js-Dateien Module sind; lassen Sie die Datei im Ordner.

Programm ausführen:

node main.js

Prüfungen ausführen (learnrun.js muss im selben Ordner liegen):

node --test
learnrun.js herunterladen

Häufige Fehler

'error' emittieren, ohne dass jemand zuhört

import {EventEmitter} from 'node:events';

const job = new EventEmitter();
job.on('done', () => console.log('done'));
job.emit('error', new Error('disk full'));
console.log('still running');

Was Node.js ausgibt

Error: disk full

Warum, und die Lösung

Ein error-Event ohne error-Listener wird geworfen: Node.js gibt den Fehler mit Stack aus, und der Prozess endet, also erscheint still running nie. Fügen Sie einen Listener hinzu, bevor etwas scheitern kann: job.on('error', (error) => console.error('job failed:', error.message)). Dann ist der Fehler nur ein Event, und das Programm läuft weiter.

super() im Konstruktor vergessen

import {EventEmitter} from 'node:events';

class Job extends EventEmitter {
  constructor(name) {
    this.name = name;
  }
}

const job = new Job('backup');
job.emit('start');

Was Node.js ausgibt

ReferenceError: Must call super constructor in derived class before accessing 'this' or returning from derived constructor

Warum, und die Lösung

Eine Klasse, die eine andere erweitert, muss in ihrem Konstruktor super() aufrufen, bevor sie this anfasst; super() richtet den Emitter ein. Schreiben Sie super(); als erste Zeile, dann this.name = name.

this in einem Pfeilfunktions-Listener

import {EventEmitter} from 'node:events';

const door = new EventEmitter();
door.on('open', () => {
  console.log('listeners:', this.listenerCount('open'));
});
door.emit('open');

Was Node.js ausgibt

TypeError: Cannot read properties of undefined (reading 'listenerCount')

Warum, und die Lösung

In einem gewöhnlichen Funktions-Listener ist this der Emitter. Eine Pfeilfunktion nimmt this von dort, wo sie geschrieben wurde, hier aus der obersten Ebene eines Moduls, wo this undefined ist. Nutzen Sie function () { … }, wenn Sie this brauchen, oder nennen Sie den Emitter einfach beim Namen: door.listenerCount("open").

JavaScript im Browser: die eigene Engine Ihres Browsers in einem abgeschotteten Worker. Syntaxfehler findet acorn 8.18.0, MIT. Lizenz und Quellcode

Abschlussquiz

5 Fragen, ohne Hinweise. Ab 80 % ist die Lektion abgeschlossen.

Erledigen Sie zuerst alle Aufgaben oben, um das Abschlussquiz freizuschalten.

Problem melden

Etwas ist falsch oder unklar? Beschreiben Sie es kurz, dann wird es geprüft und korrigiert.

#

Mindestens 20 Zeichen.

Nur, wenn Sie eine Antwort wünschen.

Kernideen

Zuhören, emittieren, aufhören

Mit import {EventEmitter} from 'node:events' und new EventEmitter() erhalten Sie einen Emitter. emitter.on('done', listener) fügt einen Listener für das Event namens done hinzu; emitter.emit('done', a, b) ruft jeden Listener von done mit den Argumenten a und b auf. once fügt einen Listener hinzu, der nach seinem ersten Aufruf entfernt wird. off('done', listener) entfernt wieder eine Instanz, und dafür braucht es dieselbe Funktion: Eine zweimal ausgeschriebene Pfeilfunktion ist eine andere Funktion, also wird nichts entfernt. on prüft nicht auf Duplikate: Dieselbe Funktion, zweimal hinzugefügt, wird zweimal aufgerufen. In einem gewöhnlichen Funktions-Listener ist this der Emitter, in einer Pfeilfunktion nicht.

Synchron, der Reihe nach, und das 'error'-Event

emit ist nicht asynchron: Es ruft die Listener sofort auf, einen nach dem anderen in der Reihenfolge des Hinzufügens, und kehrt erst zurück, wenn alle gelaufen sind. Es liefert true, wenn das Event Listener hatte, sonst false. Ein Listener, der wirft, stoppt den Rest, und der Fehler kommt aus emit heraus, wo try/catch ihn fangen kann. Das Event namens error ist besonders. Hört niemand darauf, wirft emit('error', err) den Fehler err: Node.js gibt ihn mit Stacktrace aus, und der Prozess endet. Jeder Emitter, der scheitern kann, braucht also emitter.on('error', …). Das globale EventTarget von Node verhält sich anders: Es fügt denselben Listener pro Typ nur einmal hinzu und kennt kein besonderes error-Event.

Ein eigener Emitter, ein Promise und Lecks

class Job extends EventEmitter macht jeden Job zu einem Emitter; seine Methoden rufen this.emit('progress', …) auf. Ein Konstruktor muss super() aufrufen, bevor er this benutzt. Um in asynchronem Code auf ein Event zu warten, nutzen Sie once aus node:events: const [name] = await once(job, 'done') erfüllt sich mit dem Array der Event-Argumente und lehnt ab, wenn der Emitter vorher error emittiert. Listener bleiben, bis Sie sie entfernen. Fügt man pro Anfrage einen hinzu und ruft nie off auf, wächst die Liste: Standardmäßig gibt der 11. Listener für ein Event eine MaxListenersExceededWarning aus, ein mögliches Speicherleck, doch der Listener wird trotzdem hinzugefügt. Entfernen Sie, was Sie hinzufügen, oder nutzen Sie once; setMaxListeners nur, wenn Sie es wirklich so wollen.

Quellen

Zuletzt geprüft am 4. Oktober 2026