Πώς να κωδικοποιήσετε τον δικό σας εκδότη συμβάντων στο Node.js: ένας αναλυτικός οδηγός

Κατανόηση εσωτερικών κόμβων κωδικοποιώντας μικρά πακέτα / ενότητες

Εάν είστε νέοι στο Node.js, υπάρχουν πολλά μαθήματα εδώ στο Medium και αλλού. Για παράδειγμα, μπορείτε να δείτε το άρθρο μου All About Core Node.JS.

Αλλά χωρίς άλλη παραλλαγή, ας φτάσουμε στο υπό συζήτηση θέμα: «Εκπομπές εκδηλώσεων». Οι εκπομποί συμβάντων διαδραματίζουν πολύ σημαντικό ρόλο στο οικοσύστημα Node.js.

Το EventEmitter είναι μια λειτουργική μονάδα που διευκολύνει την επικοινωνία / αλληλεπίδραση μεταξύ αντικειμένων στο Node. Το EventEmitter βρίσκεται στον πυρήνα της ασύγχρονης αρχιτεκτονικής που βασίζεται σε συμβάντα. Πολλές από τις ενσωματωμένες ενότητες του Node κληρονομούνται από το EventEmitter, συμπεριλαμβανομένων διακεκριμένων πλαισίων όπως το Express.js.

Η ιδέα είναι αρκετά απλή: τα αντικείμενα εκπομπής εκπέμπουν επώνυμα συμβάντα που προκαλούν την κλήση των ακροατών που έχουν καταχωρηθεί στο παρελθόν. Έτσι, ένα αντικείμενο εκπομπής έχει βασικά δύο κύρια χαρακτηριστικά:

  • Εκπέμπουν συμβάντα ονόματος.
  • Καταχώριση και κατάργηση εγγραφής λειτουργιών ακροατή

Είναι κάπως σαν μοτίβο σχεδιασμού παμπ / υπο ή παρατηρητή (αν και όχι ακριβώς).

Τι θα οικοδομήσουμε σε αυτό το σεμινάριο

  • Τάξη EventEmitter
  • on / addEventListener μέθοδο
  • off / removeEventListener μέθοδο
  • μια φορά τη μέθοδο
  • μέθοδος εκπομπής
  • μέθοδος rawListeners
  • μέθοδος listenerCount

Τα παραπάνω βασικά χαρακτηριστικά είναι επαρκή για την εφαρμογή ενός πλήρους συστήματος χρησιμοποιώντας το μοντέλο εξόδου.

Πριν μπείτε στην κωδικοποίηση, ας ρίξουμε μια ματιά στο πώς θα χρησιμοποιούμε την κλάση EventEmitter. Λάβετε υπόψη ότι ο κώδικας μας θα μιμείται το ακριβές API της ενότητας "συμβάντα" του Node.js.

Στην πραγματικότητα, εάν αντικαταστήσετε το EventEmitter μας με την ενσωματωμένη ενότητα "συμβάντα" του Node.js θα έχετε το ίδιο αποτέλεσμα.

Παράδειγμα 1 - Δημιουργήστε μια παρουσία εκπομπής συμβάντων και καταχωρίστε μερικές επιστροφές κλήσεων

const myEmitter = new EventEmitter(); function c1() { console.log('an event occurred!'); } function c2() { console.log('yet another event occurred!'); } myEmitter.on('eventOne', c1); // Register for eventOne myEmitter.on('eventOne', c2); // Register for eventOne

Όταν εκπέμπεται το συμβάν «eventOne», πρέπει να επικαλούνται και οι δύο παραπάνω επιστροφές κλήσεων.

myEmitter.emit('eventOne');

Η έξοδος στην κονσόλα θα έχει ως εξής:

an event occurred! yet another event occurred!

Παράδειγμα 2 - Εγγραφή στο συμβάν που θα απολυθεί μόνο μία φορά χρησιμοποιώντας μία φορά.

myEmitter.once('eventOnce', () => console.log('eventOnce once fired')); 

Εκπέμποντας το συμβάν "eventOnce":

myEmitter.emit('eventOne');

Η ακόλουθη έξοδος θα πρέπει να εμφανίζεται στην κονσόλα:

eventOnce once fired

Η εκπομπή συμβάντων που έχουν εγγραφεί για άλλη μια φορά δεν θα έχει αντίκτυπο.

myEmitter.emit('eventOne');

Εφόσον το συμβάν εκπέμπεται μόνο μία φορά, η παραπάνω δήλωση δεν θα έχει αντίκτυπο.

Παράδειγμα 3 - Εγγραφή στο συμβάν με παραμέτρους επανάκλησης

myEmitter.on('status', (code, msg)=> console.log(`Got ${code} and ${msg}`));

Εκπέμποντας το συμβάν με παραμέτρους:

myEmitter.emit('status', 200, 'ok');

Η έξοδος στην κονσόλα θα έχει ως εξής:

Got 200 and ok

ΣΗΜΕΙΩΣΗ: Μπορείτε να εκπέμψετε συμβάντα πολλές φορές (εκτός από αυτά που έχουν καταχωριστεί με τη μέθοδο μία φορά).

Παράδειγμα 4 - Κατάργηση εγγραφής συμβάντων

myEmitter.off('eventOne', c1);

Τώρα, εάν εκπέμψετε το συμβάν ως εξής, δεν θα συμβεί τίποτα και θα είναι ένα noop:

myEmitter.emit('eventOne'); // noop

Παράδειγμα 5 - Λήψη του αριθμού ακροατών

console.log(myEmitter.listenerCount('eventOne'));

ΣΗΜΕΙΩΣΗ: Εάν το συμβάν έχει καταγραφεί χρησιμοποιώντας τη μέθοδο off ή removeListener, τότε η μέτρηση θα είναι 0.

Παράδειγμα 6 - Λήψη ακατέργαστων ακροατών

console.log(myEmitter.rawListeners('eventOne'));

Παράδειγμα 7 - Παράδειγμα επίδειξης Async

// Example 2->Adapted and thanks to Sameer Buna class WithTime extends EventEmitter { execute(asyncFunc, ...args) { this.emit('begin'); console.time('execute'); this.on('data', (data)=> console.log('got data ', data)); asyncFunc(...args, (err, data) => { if (err) { return this.emit('error', err); } this.emit('data', data); console.timeEnd('execute'); this.emit('end'); }); } }

Χρήση του εκδότη συμβάντος withTime:

const withTime = new WithTime(); withTime.on('begin', () => console.log('About to execute')); withTime.on('end', () => console.log('Done with execute')); const readFile = (url, cb) => { fetch(url) .then((resp) => resp.json()) // Transform the data into json .then(function(data) { cb(null, data); }); } withTime.execute(readFile, '//jsonplaceholder.typicode.com/posts/1');

Ελέγξτε την έξοδο στην κονσόλα. Η λίστα των δημοσιεύσεων θα εμφανίζεται μαζί με άλλα αρχεία καταγραφής.

Το Σχέδιο Παρατηρητή για την Εκδήλωση Εκπομπής

Οπτικό διάγραμμα 1 (Μέθοδοι στο EventEmitter)

Εφόσον τώρα καταλαβαίνουμε το API χρήσης, ας αρχίσουμε να κωδικοποιούμε τη λειτουργική μονάδα.

Ο πλήρης κωδικός boilerplate για την κατηγορία EventEmitter

Θα συμπληρώσουμε σταδιακά τις λεπτομέρειες στις επόμενες ενότητες ζευγών.

class EventEmitter { listeners = {}; // key-value pair addListener(eventName, fn) {} on(eventName, fn) {} removeListener(eventName, fn) {} off(eventName, fn) {} once(eventName, fn) {} emit(eventName, ...args) { } listenerCount(eventName) {} rawListeners(eventName) {} }

We begin by creating the template for the EventEmitter class along with a hash to store the listeners. The listeners will be stored as a key-value pair. The value could be an array (since for the same event we allow multiple listeners to be registered).

1. The addListener() method

Let us now implement the addListener method. It takes in an event name and a callback function to be executed.

 addListener(event, fn)  []; this.listeners[event].push(fn); return this; 

A little explanation:

The addListener event checks if the event is already registered. If yes, returns the array, otherwise empty array.

this.listeners[event] // will return array of events or undefined (first time registration)

For example…

Let’s understand this with a usage example. Let’s create a new eventEmitter and register a ‘test-event’. This is the first time the ‘test-event’ is being registered.

const eventEmitter = new EventEmitter(); eventEmitter.addListener('test-event', ()=> { console.log ("test one") } );

Inside addListener () method:

this.listeners[event] => this.listeners['test-event'] => undefined || [] => []

The result will be:

this.listeners['test-event'] = []; // empty array

and then the ‘fn’ will be pushed to this array as shown below:

this.listeners['test-event'].push(fn);

I hope this makes the ‘addListener’ method very clear to decipher and understand.

A note: Multiple callbacks can be registered against that same event.

2. The on method

This is just an alias to the ‘addListener’ method. We will be using the ‘on’ method more than the ‘addListener’ method for the sake of convenience.

on(event, fn) { return this.addListener(event, fn); }

3. The removeListener(event, fn) method

The removeListener method takes an eventName and the callback as the parameters. It removes said listener from the event array.

NOTE: If the event has multiple listeners then other listeners will not be impacted.

First, let’s take a look at the full code for removeListener.

removeListener (event, fn) { let lis = this.listeners[event]; if (!lis) return this; for(let i = lis.length; i > 0; i--) { if (lis[i] === fn) { lis.splice(i,1); break; } } return this; }

Here’s the removeListener method explained step-by-step:

  • Grab the array of listeners by ‘event’
  • If none found return ‘this’ for chaining.
  • If found, loop through all listeners. If the current listener matches with the ‘fn’ parameter use the splice method of the array to remove it. Break from the loop.
  • Return ‘this’ to continue chaining.

4. The off(event, fn) method

This is just an alias to the ‘removeListener’ method. We will be using the ‘on’ method more than the ‘addListener’ method for sake of convenience.

 off(event, fn) { return this.removeListener(event, fn); }

5. The once(eventName, fn) method

Adds a one-timelistener function for the event named eventName. The next time eventName is triggered, this listener is removed and then invoked.

Use for setup/init kind of events.

Let’s take a peek at the code.

once(eventName, fn) { this.listeners[event] = this.listeners[eventName] || []; const onceWrapper = () => { fn(); this.off(eventName, onceWrapper); } this.listeners[eventName].push(onceWrapper); return this; }

Here’s the once method explained step-by-step:

  • Get the event array object. Empty array if the first time.
  • Create a wrapper function called onceWrapper which will invoke the fn when the event is emitted and also removes the listener.
  • Add the wrapped function to the array.
  • Return ‘this’ for chaining.

6. The emit (eventName, ..args) method

Synchronously calls each of the listeners registered for the event named eventName, in the order they were registered, passing the supplied arguments to each.

Returns true if the event had listeners, false otherwise.

emit(eventName, ...args) { let fns = this.listeners[eventName]; if (!fns) return false; fns.forEach((f) => { f(...args); }); return true; }

Here’s the emit method explained step-by-step:

  • Get the functions for said eventName parameter
  • If no listeners, return false
  • For all function listeners, invoke the function with the arguments
  • Return true when done

7. The listenerCount (eventName) method

Returns the number of listeners listening to the event named eventName.

Here’s the source code:

listenerCount(eventName) 

Here’s the listenerCount method explained step-by-step:

  • Get the functions/listeners under consideration or an empty array if none.
  • Return the length.

8. The rawListeners(eventName) method

Returns a copy of the array of listeners for the event named eventName, including any wrappers (such as those created by .once()). The once wrappers in this implementation will not be available if the event has been emitted once.

rawListeners(event) { return this.listeners[event]; }

The full source code for reference:

class EventEmitter { listeners = {} addListener(eventName, fn)  on(eventName, fn) { return this.addListener(eventName, fn); } once(eventName, fn) { this.listeners[eventName] = this.listeners[eventName] || []; const onceWrapper = () => { fn(); this.off(eventName, onceWrapper); } this.listeners[eventName].push(onceWrapper); return this; } off(eventName, fn) { return this.removeListener(eventName, fn); } removeListener (eventName, fn) { let lis = this.listeners[eventName]; if (!lis) return this; for(let i = lis.length; i > 0; i--) { if (lis[i] === fn) { lis.splice(i,1); break; } } return this; } emit(eventName, ...args) { let fns = this.listeners[eventName]; if (!fns) return false; fns.forEach((f) => { f(...args); }); return true; } listenerCount(eventName)  rawListeners(eventName) { return this.listeners[eventName]; } }

The complete code is available here:

//jsbin.com/gibofab/edit?js,console,output

As an exercise feel free to implement other events’ APIs from the documentation //nodejs.org/api/events.html.

If you liked this article and want to see more of similar articles, feel free to give a couple of claps :)

ΣΗΜΕΙΩΣΗ : Ο κώδικας έχει βελτιστοποιηθεί για αναγνωσιμότητα και όχι για απόδοση. Ίσως ως άσκηση, μπορείτε να βελτιστοποιήσετε τον κώδικα και να τον μοιραστείτε στην ενότητα σχολίων. Δεν έχω δοκιμάσει πλήρως για περιθωριακές περιπτώσεις και ορισμένες επικυρώσεις ενδέχεται να είναι απενεργοποιημένες καθώς αυτό ήταν μια γρήγορη εγγραφή.

Αυτό το άρθρο είναι μέρος του επερχόμενου βίντεο μαθήματος "Node.JS Master Class - Δημιουργήστε το δικό σας ExpressJS-MVC Framework από το μηδέν"

Ο τίτλος του μαθήματος δεν έχει ακόμη οριστικοποιηθεί.