Cara membuat kod pemancar acara anda sendiri di Node.js: panduan langkah demi langkah

Fahami dalaman Node dengan mengekod pakej / modul kecil

Sekiranya anda baru menggunakan Node.js terdapat banyak tutorial di sini di Medium dan di tempat lain. Anda boleh melihat artikel saya Semua Tentang Core Node.JS, misalnya.

Tetapi tanpa basa-basi lagi, mari kita pergi ke topik yang dibincangkan: "Pemancar Acara". Pemancar Acara memainkan peranan yang sangat penting dalam ekosistem Node.js.

EventEmitter adalah modul yang memudahkan komunikasi / interaksi antara objek di Node. EventEmitter adalah teras kepada senibina yang diselaraskan oleh Node yang tidak segerak. Sebilangan besar modul terbina dalam Node mewarisi dari EventEmitter termasuk kerangka kerja yang menonjol seperti Express.js.

Konsepnya agak mudah: objek pemancar memancarkan peristiwa bernama yang menyebabkan pendengar yang terdaftar sebelumnya dipanggil. Jadi, objek pemancar pada dasarnya mempunyai dua ciri utama:

  • Memancarkan acara nama.
  • Mendaftar dan membatalkan pendaftaran fungsi pendengar.

Ini seperti corak reka bentuk pub / sub atau pemerhati (walaupun tidak tepat).

Apa yang akan kita buat dalam tutorial ini

  • Kelas EventEmitter
  • kaedah on / addEventListener
  • kaedah off / removeEventListener
  • kaedah sekali
  • kaedah memancarkan
  • kaedah rawListeners
  • kaedah listenerCount

Ciri-ciri asas di atas mencukupi untuk menerapkan sistem penuh menggunakan model eventing.

Sebelum kita memasuki pengkodan, mari kita lihat bagaimana kita akan menggunakan kelas EventEmitter. Harap maklum bahawa kod kami akan meniru API yang tepat dari modul 'acara' Node.js.

Sebenarnya, jika anda mengganti EventEmitter kami dengan modul 'acara' terbina dalam Node.js anda akan mendapat hasil yang sama.

Contoh 1 - Buat contoh pemancar peristiwa dan daftarkan beberapa panggilan balik

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

Apabila acara 'eventOne' dipancarkan, kedua-dua panggilan balik di atas harus dipanggil.

myEmitter.emit('eventOne');

Keluaran di konsol akan seperti berikut:

an event occurred! yet another event occurred!

Contoh 2 - Mendaftar untuk acara yang akan dipecat hanya sekali menggunakan sekali.

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

Memancarkan acara 'eventOnce':

myEmitter.emit('eventOne');

Keluaran berikut akan muncul di konsol:

eventOnce once fired

Memancarkan acara yang didaftarkan sekali lagi tidak akan memberi kesan.

myEmitter.emit('eventOne');

Oleh kerana acara tersebut hanya dipancarkan sekali, pernyataan di atas tidak akan memberi kesan.

Contoh 3 - Mendaftar untuk acara dengan parameter panggilan balik

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

Memancarkan acara dengan parameter:

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

Keluaran di konsol adalah seperti di bawah:

Got 200 and ok

CATATAN: Anda boleh memancarkan acara berkali-kali (kecuali peristiwa yang didaftarkan dengan kaedah sekali).

Contoh 4 - Membatalkan pendaftaran acara

myEmitter.off('eventOne', c1);

Sekarang jika anda memancarkan acara seperti berikut, tidak ada yang akan berlaku dan ia akan berlangsung:

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

Contoh 5 - Mendapatkan jumlah Pendengar

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

CATATAN: Sekiranya acara tidak didaftarkan menggunakan kaedah off atau removeListener, maka jumlahnya adalah 0.

Contoh 6 - Mendapatkan Pendengar Mentah

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

Contoh 7— Demo Contoh 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'); }); } }

Menggunakan pemancar acara 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');

Periksa output di konsol. Senarai catatan akan dipaparkan bersama dengan log lain.

Corak Pemerhati untuk Pemancar Acara Kami

Gambarajah Visual 1 (Kaedah dalam EventEmitter kami)

Oleh kerana sekarang kita memahami penggunaan API, mari kita mengekod modul.

Kod boilerplate yang lengkap untuk kelas EventEmitter

Kami akan mengisi butirannya secara bertahap di bahagian pasangan seterusnya.

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 :)

NOTE: The code is optimized for readability and not for performance. Maybe as an exercise, you can optimize the code and share it in the comment section. Haven’t tested fully for edge cases and some validations may be off as this was a quick writeup.

This article is part of the upcoming video course “Node.JS Master Class — Build Your Own ExpressJS-Like MVC Framework from scratch”.

The title of the course is not yet finalized.