RxJS, the stream-based library in JavaScript (JS), popularized the use of the event-based system in building JS apps. Node.js comes with the built-in function EventEmitter
, which uses the producer-consumer pattern. The popular JS library SocketIO
uses this event-based architecture to implement WebSocket
, while other libraries have used it for
These libraries are based on the producer-consumer pattern. The producer produces an event and a listening consumer consumes the event.
Producers are the sources of your data. A stream must always have a producer of data. The producer is created from something that independently generates events (anything from a single value, array, or mouse clicks, to a stream of bytes that is read from a file).
Consumers accept events from the producer and process them in a specific way.
Let’s use the events in Node.js for a demonstration:
const EventEmitter = require("events")
Calculator extends EventEmitter {}
const calc = new Calculator()
calc.addEventListener("result", (result)=> log(result))
calc.addEventListener("add", (a,b)=> {
log("Adding "+ a +" to " + b)
calc.emit('result', a + b)
})
calc.addEventListener("subtract", (a,b)=> {
log("Subtracting "+ b +" from " + a)
calc.emit('result', b - a)
})
calc.emit('add', 1, 2)
calc.emit('subtract', 4, 2)
In the code above, the Calculator
class extends the EventEmitter
. Then, Calculator
registers events [result
, add
, and subtract
] with the addEventListener
method, which is inherited from EventEmitter
. addEventListener
is a consumer, so it listens to the events, and when one is produced addEventListener
consumes it.
We create three consumers: add
, subtract
, and result
. The add
consumer listens to the ‘add’ event. add
will accept the values produced through its function callback parameters a
and b
. The subtract
consumer works similarly. The result
consumer waits for the ‘result’ event and will display the result when the result
event is produced.
The emit
method is inherited from the EventEmitter
and serves as the producer. emit
produces the events add
, subtract
, and result
, and generates events with data.
The consumers of these events (the callbacks of Calculator#addEventListener
) will be called with the data.
Let’s use a real-life scenario to depict the producer-consumer pattern. Obi has a ball and assembles three boys, Ed, Young, and Figo. Then, he tells Ed, “If I say ‘Stop,’ take the ball and perform the action ‘Stop.’” He tells Young if he says “Start,” he should take the ball and perform the action “Start.” For Figo, “Yell” should make him perform the action “Yell.”
After these instructions, the three boys will be listening to Obi for the code word.
Here, Obi is the producer. He will produce the sound codeword with the data, i.e., the ball. Ed, Young, and Figo are the consumers, as they are listening to consume what Obi (the producer) will say.
When Obi says “Yell,” all three of them hear it but the codeword is meant for Figo, so only he will start the action, while Young and Ed keep listening. When Figo is done, he comes back and starts listening again. If Obi says “Run,” nobody will perform an action because the word is not the codeword for any one of them.
The code words are like the events in our code above. The three boys listening to Obi is like Calculator#addEventListeners
add
, subtract
, and result
. And Obi saying a codeword is similar to Calculator#emit
.
EventEmitter
First, create a Node project as shown below:
mkdir event-emitter
cd event-emitter
npm init -y
touch events.js
You will have the following code:
- event-emitter
- package.json
- events.js
Open events.js
to make the edits.
Let’s create an Events
class:
class Events {}
We will add a constructor that contains a listeners
object literal. This object is where listeners will be stored in a key-value fashion.
class Events {
constructor() {
this.listeners = {}
}
}
Remember, all listeners or observers listen to an event name. The event name will be the key. The listeners have function callbacks that are to be executed when an event name is produced, and the function callback will be the value.
EventEmitter
has the on
, once
, and addEventListener
methods.
The on
method is used to add a listener. on
takes the event name and function callback as arguments.
Let’s implement the method:
class Event {
constructor() {
this.listeners = {}
}
on(str, fn) {
this.listeners[str] = fn
}
}
str
holds the event name and fn
holds the function callback. We add it to the listeners
array, with str
as the key and fn
as the value.
The once
method is used to register an event. The event is removed if the event is emitted and runs once.
class Event {
constructor() {
this.listeners = {}
}
on(str, fn) {
this.listeners[str] = fn
}
once(str, fn) {
this.listeners[str] = fn
}
}
addEventListener
is the same as the on
method.
class Event {
constructor() {
this.listeners = {}
}
on(str, fn) {
this.listeners[str] = fn
}
once(str, fn) {
this.listeners[str] = fn
}
addEventListener(str,fn) {
this.on(str,fn)
}
}
There is a call to on
with its parameters within addEventListener
.
Let’s implement the emit
method to produce an event with optional data:
class Event {
constructor() {
this.listeners = {}
}
on(str, fn) {
this.listeners[str] = fn
}
once(str, fn) {
this.listeners[str] = fn
}
addEventListener(str,fn) {
this.on(str,fn)
}
emit(str, data) {
this.listeners[str](data)
}
}
emit
takes the name of the event to be emitted in the str
parameter and the data in the data
parameter. emit
uses str
as a key to get the function callback associated with it in the listeners
object, this.listeners[str]
. This returns the function callback, which calls this.listeners[str](data)
and passes in the data in the data
parameter.
However, with the implementation of emit
, the once
method won’t work. once
is supposed to remove an event once it is run.
Let’s fix this:
class Events {
constructor() {
this.listeners = {}
}
on(str, fn) {
this.listeners[str] = fn
}
emit(str, data) {
this.listeners[str](data)
}
once(str, fn) {
const self = this
function onceFn(data) {
self.removeEventListener(str)
fn(data)
}
this.on(str,onceFn)
}
addEventListener(str,fn) {
this.on(str,fn)
}
removeEventListener(str) {
delete this.listeners[str]
}
}
We create a function onceFn
inside the once
method. This creates a closure around the onceFn
function. We store the instance to self
and call the on
method, passing str
and the onceFn
function that will be the callback. Inside the onceFn
function, it uses the self
variable to get access to the Events
class instance to call its Event#removeEventListener
to remove the event.
Next, this
calls the original function callback, fn
. Note that onceFn
is able to access variables outside its scope because of closure. This will effectively make events registered with the once
method be emitted and called once.
const events = new Events()
events.once("once",()=>l("once"))
1. events.emit("once")
2. events.emit("once")
The second once
emission in the code snippet above will throw an error.
Note: Registering two events with the same name will choose the latter and override the rest.
Let’s use our previous Calculator
to test our implementation:
const calc = new Events()
calc.addEventListener("result", (result)=> log(result))
calc.addEventListener("add", (a,b)=> {
log("Adding "+ a +" to " + b)
calc.emit('result', a + b)
})
calc.addEventListener("subtract", (a,b)=> {
log("Subtracting "+ a +" from " + b)
calc.emit('result', a - b)
})
calc.emit('add', 1, 2)
calc.emit('subtract', 4, 2)
Now, let’s run it:
$ node events
Adding 1 to 2
3
Subtracting 4 from 2
2
We can share function utils to Bit. Since they are reusable, they can be shared with other developers, which Bit does with a Node playground for developers to test run the code before they install it.
To begin, we have to install the Bit CLI tool:
$ npm i bit-cli -g
We initialize a Bit environment in our project:
$ bit init
Next, we add the files we want to move to Bit:
$ bit add events
tracking 1 new component
Then, we install the Babel compiler:
$ bit import bit.envs/compilers/babel --compiler
the following component environments were installed
- bit.envs/compilers/babel@0.0.20
This command not only installs the compilers/babel
extension but also sets it as a default build step for all components that originate from your workspace. You can see it configured in your package.json
file:
{
"bit": {
"env": {
"compiler": "bit.envs/compilers/babel@0.0.20"
},
"componentsDefaultDirectory": "components/{name}",
"packageManager": "npm"
}
}
We build and tag with version:
$ bit tag --all 0.0.1
Bit tags the component with version . The component is also built during this tagging process. Bit runs all component extensions during the tagging process to validate that it can recreate all components in an isolated environment and run all tasks, e.g., build and test. Bit fails the versioning process if it is unable to isolate a component.
Before launch, you need to create a collection, which you can do here
With your account name and collection name handy, you can launch:
$ bit export YOUR_ACCOUNT_NAME_HERE.YOUR_COLLECTION_NAME_HERE
After a successful launch, go to your collections page and you will see your component there. You can now play with the event emitter.
Note: You can also view the full code here.