back to top

Node.js Event Emitter

Al termine della lezione in cui abbiamo parlato dell’Event Loop, abbiamo introdotto velocemente la ‘classe’ Event Emitter e abbiamo visto un semplice esempio in cui abbiamo fatto uso del metodo process.nexTick(). Tratteremo di nuovo l’argomento in maniera un po’ più approfondita in questa lezione, cercando di capire come funziona un Event Emitter, dal momento che si tratta di uno dei principi fondamentali utilizzati in diversi moduli nativi. Sono infatti numerosi gli oggetti, costruiti su questo modello, che, essendo delle istanze della classe Event Emitter, emettono degli eventi i quali verranno ricevuti da tutte le funzioni (listener) che hanno chiesto di essere notificate ogni volta che viene lanciato un determinato evento. Ribadiamo il fatto che la classe Event Emitter non hanno nulla a che fare con il sistema precedentemente descritto dell’Event Loop e gli eventi del sistema operativo.

Introduzione

Abbiamo già detto che ogni oggetto di tipo Event Emitter ha due metodi fondamentali: emitter.emit() e emitter.on() (emitter.addListener() è un alias del metodo emitter.on()). Con il metodo on(nomeEvento, listener) registriamo una funzione callback (listener) da invocare ogni volta che viene lanciato l’evento nomeEvento attraverso la funzione emit(nomeEvento, […argomentiDaPassareAlleCallbackListener]). Notate bene che quando un oggetto emette un evento (ovvero quando invoca la funzione emit()), tutte le funzioni listener che si erano registrate per quell’evento vengono invocate in ordine e in maniera sincrona. Il comportamento predefinito prevede che si possano registrare fino a un massimo di 10 listener per ogni evento (EventEmitter.defaultMaxListeners). Per ogni istanza possiamo all’occorrenza settare un nuovo valore usando il metodo eventEmitter.setMaxListeners(n) (n deve essere un numero intero positivo). Allo stesso tempo possiamo visualizzare il numero massimo di listener corrente con il metodo eventEmitter.getMaxListeners()

Versione semplificata di Event Emitter

Per comprendere meglio come funziona la classe Event Emitter e in particolare i metodi emit() e on(), implementiamo una nostra versione rudimentale. Creiamo quindi due file index.js e EventEmitter.js all’interno di una nuova directory.

class EventEmitter {
  constructor() {
    this.eventi = {};
  }

  on(nomeEvento, listener) {
    if ( typeof nomeEvento !== 'string' ) {
      throw new TypeError('Il primo argomento 'nomeEvento' deve essere una stringa');
    }

    if ( typeof listener !== 'function' ) {
      throw new TypeError('Il secondo argomento 'listener' deve essere una funzione');
    }

    this.eventi[nomeEvento] =  this.eventi[nomeEvento] || [];
    this.eventi[nomeEvento].push(listener);
  }

  emit(nomeEvento, ...args) {
    if ( typeof nomeEvento !== 'string' ) {
      throw new TypeError('Il primo argomento \'nomeEvento\' deve essere una stringa');
    }

    const listenersEvento = this.eventi[nomeEvento];

    if ( listenersEvento ) {
      listenersEvento.forEach((listener) => {
        listener.apply(this, args);
      });
    }
  }
}

module.exports = EventEmitter;

Abbiamo creato la nostra versione semplificata di EventEmitter. Usiamo il metodo EventEmitter.prototype.on() per registrare nuove funzioni listener per ciascun tipo di evento. Con la funzione EventEmitter.prototype.emit() invochiamo invece in ordine tutte le funzioni registrate per un dato evento. Notate che tutto si svolge in maniera sincrona. Come specificato nella documentazione, all’interno di EventEmitter.prototype.emit() invochiamo i vari listener in modo che il valore di this sia un riferimento all’oggetto EventEmitter.

const EventEmitter = require('./EventEmitter');

const emitter = new EventEmitter();

const listener1 = (...args) => console.log(...args, this);

emitter.on('evento', listener1);
emitter.on('evento', function(a, b) {console.log(a, b, this)});

emitter.emit('evento', 1, 2, [6,7,8]);

console.log(emitter.eventi); // { evento: [ [Function: listener1], [Function] ] }

Nel file index.js importiamo il nostro EventEmitter, creiamo un’istanza che chiamiamo banalmente emitter e registriamo due funzioni che verranno invocate nel momento in cui eseguiamo emitter.emit().Nella shell visualizzeremo un output simile a quello riportato sotto.

// output di listener1
1 2 [ 6, 7, 8 ] {}

// output della funzione anonima
1 2 EventEmitter { eventi: { evento: [ [Function: listener1], [Function] ] } }

// emitter.eventi
{ evento: [ [Function: listener1], [Function] ] }

Notate che, nel caso della funzione listener1, il valore di this non è uguale all’istanza emitter di EventEmitter. Ciò è dovuto al comportamento specifico delle arrow function. (Se volete approfondire l’argomento, potete trovare maggiori dettagli sul sito di MDN web docs)

Esempi con oggetti di tipo Event Emitter

Dopo aver visto una nostra semplice implementazione, vediamo due diversi esempi in cui usiamo la classe Event Emitter messa a disposizione da Node.js.

Esempio 1: istanza di Event Emitter

Come primo esempio, creiamo un’istanza di Event Emitter e registriamo alcune funzioni con i metodi EventEmitter.on() e EventEmitter.prependEmitter().

const EventEmitter = require('events');

const emitter = new EventEmitter();

emitter.on(
  'evento1', 
  () => console.log('Seconda funzione ad essere invocata')
);
emitter.on(
  'evento1', 
  () => console.log('Terza funzione ad essere invocata')
);

emitter.prependListener(
  'evento1', 
  () => console.log('Prima funzione ad essere invocata')
);

emitter.emit('evento1');

In questo esempio non abbiamo usato nulla di nuovo a parte il metodo emitter.prependListener(). Come già detto, le funzioni listener vengono invocate nell’ordine in cui vengono registrate. Con questo metodo possiamo inserire una funzione all’inizio dell’array e invocarla prima degli altri listener.

Esempio 2: estendiamo la classe base Event Emitter

Vediamo un altro modo in cui possiamo usare gli Event Emitter, ovvero attraverso il meccanismo dell’ereditarietà. Creiamo due nuovi file Emitter.js e app.js. Nel primo inseriamo una nuova classe Emitter che estende la classe base EventEmitter e ne eredita quindi i metodi.

// file Emitter.js

const EventEmitter = require('events');

class Emitter extends EventEmitter {}

module.exports = Emitter;

Nel file app.js realizziamo una funzione all’interno della quale creiamo una nuova istanza di Emitter (class Emitter extends EventEmitter {}) e lanciamo un evento. L’istanza emitter creata viene restituita come valore di ritorno. All’interno della funzione emitEvent() abbiamo usato process.nextTick() per assicurarci che i vari listener, registrati dopo l’esecuzione della funzione, vengano eseguiti. (Come avevamo già fatto nell’esempio visto nella lezione in cui abbiamo parlato dell’Event Loop)

// file app.js

const Emitter = require('./Emitter');

function emitEvent(nomeEvento) {
  const emitter = new Emitter();
  process.nextTick(emitter.emit.bind(emitter), nomeEvento);
  return emitter;
}

const evento = 'prova';

const emitter = emitEvent(evento);

emitter.once(evento, () => console.log('Listener registrato con once()'))
emitter.on(evento, () => console.log('Listener registrato con on()'))

emitter.emit(evento);

Dopo aver registrato due listener, lanciamo nuovamente l’evento ‘prova’. In questo modo verifichiamo che tutte le funzioni registrate con il metodo emitter.once() vengono invocate una sola volta.

Listener registrato con once()
Listener registrato con on()
Listener registrato con on()

Event Emitter e gestione degli errori

La documentazione di Node.js consiglia di registrare sempre, per ogni istanza di Event Emitter, almeno una funzione listerner per un tipo di evento particolare, ovvero l’evento ‘error’. Quando si lavora con gli oggetti EventEmitter è pratica comune quella di emettere l’evento ‘error’ se si verificano degli errori. Se non è presente almeno una funzione listener registrata l’applicazione va in crash.

const EventEmitter = require('events');

const emitter = new EventEmitter();

emitter.on('error', (errore) => {
  console.error(`Si è verificato un errore, ${errore}!!!`);
});

emitter.emit('error', new Error('ERRORE'));

Nell’esempio appena visto, verrà stampato un messaggio di errore nella shell.

Si è verificato un errore Error: ERRORE

Per evitare che l’applicazione vada in crash in assenza di listener per l’evento ‘error’, possiamo registrare una funzione listener per l’evento ‘uncaughtException’ emesso dall’oggetto process. (process è un EventEmitter).

const EventEmitter = require('events');

const emitter = new EventEmitter();

process.on('exit', (codice) => {
  console.log(`Il processo sta per terminare con codice: ${codice}`);
});

process.on('uncaughtException', (errore) => {
  console.error('Si è verificato un errore! L'applicazione verrà terminata...');
  process.exit(1);
});

emitter.emit('error', new Error('ERRORE!'));

Nella console verrà mostrato il seguente output.

Si è verificato un errore! L'applicazione verrà terminata...
Il processo sta per terminare con codice: 1

Se invece aggiungiamo una funzione listener per l’evento ‘error’, non sarà lanciato l’evento uncaughtException, come potete vedere nell’esempio che segue. Quando il processo terminerà, lo farà con il codice d’uscita 0 invece di 1 che solitamente viene usato per indicare una terminazione del processo a causa di qualche problema o errore.

const EventEmitter = require('events');

const emitter = new EventEmitter();

process.on('exit', (codice) => {
  console.log(`Il processo sta per terminare con codice: ${codice}`);
});

process.on('uncaughtException', (errore) => {
  console.error('Si è verificato un errore! L\'applicazione verrà terminata...');
  process.exit(1);
});

emitter.on('error', (errore) => {
  console.error(`Si è verificato un errore ${errore}`);
});

emitter.emit('error', new Error('ERRORE!'));

Conclusioni

Anche per questa lezione è tutto. Abbiamo visto come realizzare una implementazione semplificata di Event Emitter. Abbiamo mostrato, inoltre, alcuni esempi realizzati facendo uso della ‘classe’ Event Emitter messa a disposizione da Node.js. Nel prossimo vedremo cosa sono e come usare gli oggetti di tipo Buffer.

Pubblicitร