- 1. Node.js TypeScript #1. Modules, process arguments, basics of the File System
- 2. Node.js TypeScript #2. The synchronous nature of the EventEmitter
- 3. Node.js TypeScript #3. Explaining the Buffer
- 4. Node.js TypeScript #4. Paused and flowing modes of a readable stream
- 5. Node.js TypeScript #5. Writable streams, pipes, and the process streams
- 6. Node.js TypeScript #6. Sending HTTP requests, understanding multipart/form-data
- 7. Node.js TypeScript #7. Creating a server and receiving requests
- 8. Node.js TypeScript #8. Implementing HTTPS with our own OpenSSL certificate
- 9. Node.js TypeScript #9. The Event Loop in Node.js
- 10. Node.js TypeScript #10. Is Node.js single-threaded? Creating child processes
- 11. Node.js TypeScript #11. Harnessing the power of many processes using a cluster
- 12. Node.js TypeScript #15. Benefits of the HTTP/2 protocol
- 13. Node.js TypeScript #12. Introduction to Worker Threads with TypeScript
- 14. Node.js TypeScript #13. Sending data between Worker Threads
- 15. Node.js TypeScript #14. Measuring processes & worker threads with Performance Hooks
In this article, we continue going through the main concepts connected to Node.js. This time we dive into the idea of the Node.js EventEmitter. We explain its synchronous nature and how it works, which helps with understanding other Node.js features because some of them use the EventEmitter under the hood.
Node.js TypeScript: EventEmitter
Using events is a big part of working with JavaScript and therefore. A lot of Node.js core depends on event-driven architecture. Because of it, a lot of objects that you can encounter inherit from the EventEmmiter.
Certain objects can emit events and we call them emitters. We can listen to those and react in a way using callback functions called listeners.
An instance of the EventEmmiter has a method “on” that allows one or more function to be attached to an object.
The instance of the EventEmmiter also has a method “emit” that emits an event and causes all the EventEmitter to call all the listeners.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 |
import * as EventEmitter from 'events'; const eventEmitter = new EventEmitter(); eventEmitter.on('event', function() { console.log('one'); }); eventEmitter.on('event', function() { console.log('two'); }); eventEmitter.on('event', function() { console.log('three'); }); eventEmitter.emit('event'); |
one
two
three
The EventEmitter called all of the above functions synchronously. If you look into the Node.js source code, you can confirm that they run in an order that they were attached.
The “on” function is an alias for “addEventListener” and both functions act the same way
You can also observe that if you attach a listener after you call the emit function, the EventEmitter does not call it.
1 2 3 4 5 6 7 8 9 |
import * as EventEmitter from 'events'; const eventEmitter = new EventEmitter(); eventEmitter.emit('event'); eventEmitter.on('event', function() { console.log('Event occured!'); // not logged into the console }); |
It is an important thing to keep in mind especially if you are more familiar with promises. It is due to the fact that with promises you can add callbacks long after a promise is resolved and it is going to be called immediately.
If you want to know more about promises, check out Explaining promises and callbacks while implementing a sorting algorithm
Passing additional data to listeners
The emit function allows you to send arguments to the listener functions. Inside of a listener function, the “this” keyword refers to the instance of the Event Emitter:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 |
import * as EventEmitter from 'events'; const eventEmitter = new EventEmitter(); eventEmitter.on('event', function(data) { console.log(data); // { key: value } console.log(this === eventEmitter); // true }); eventEmitter.emit( 'event', { key: 'value' } ); |
A lot of event emitters that you encounter pass additional arguments to listeners.
If you use the arrow functions here, the “this” keyword no longer references to the Node.js EventEmitter instance.
1 2 3 4 5 6 7 8 9 |
import * as EventEmitter from 'events'; const eventEmitter = new EventEmitter(); eventEmitter.on('event', () => { console.log(this === eventEmitter); // false }); eventEmitter.emit('event'); |
This behavior is caused by the way that arrow functions behave. If you want to know more check out What is “this”? Arrow functions
Removing listeners
If you don’t want the EventEmitter to invoke a listener anymore, you can use the removeListener function. By calling it, you remove the listener from a particular event. To do that, you need to provide the removeListener function with the name of the event, and the reference to the callback function.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 |
import * as EventEmitter from 'events'; const eventEmitter = new EventEmitter(); function listener () { console.log('Event occurred!'); } eventEmitter.on('event', listener); eventEmitter.emit('event'); // Event occurred! eventEmitter.removeListener('event', listener); eventEmitter.emit('event'); /// Nothing happened |
Please note that if you added more than one instance of a listener, you need to remove it more than once to get rid of it. You can also use the removeAllListeners function to remove all listeners for a particular event.
The synchronous nature of events
As said above, Node.js EventEmmiter calls all listeners synchronously. We can observe it by calling them into one another:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 |
import * as EventEmitter from 'events'; const eventEmitter = new EventEmitter(); eventEmitter.on('event1', () => { console.log('First event here!'); eventEmitter.emit('event2'); }); eventEmitter.on('event2', () => { console.log('Second event here!'); eventEmitter.emit('event3'); }); eventEmitter.on('event3', () => { console.log('Third event here!'); eventEmitter.emit('event1'); }); eventEmitter.emit('event1'); |
After a bunch of messages in the console, you encounter an error:
Maximum call stack size exceeded
The error is thrown because the actual execution of the above listeners looks like this:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 |
function event1() { console.log('First event here!'); event2(); } function event2() { console.log('Second event here!'); event3(); } function event3() { console.log('Third event here!'); event1(); } event1(); |
The event emitter executes all callbacks in a synchronous manner. Every time you call a function, its context is pushed on the top of the call stack. When the function ends, the context is taken off from the stack. If you call one function inside of the other, the data can pile up in your call stack and eventually cause it to overflow.
I dive deeper into this context in Using recursion to traverse data structures. Execution context and the call stack
You can change this behavior a bit by using, for example, the setTimeout function. When the Node.js interprets the setTimeout function, it sets off a timer. When it goes off, it throws the callback to the end of the event loop. Due to that, one function is not called inside of the other one and the call stack is not exceeded.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 |
import * as EventEmitter from 'events'; const eventEmitter = new EventEmitter(); eventEmitter.on('event1', () => { setTimeout(() => { console.log('First event here!'); eventEmitter.emit('event2'); }) }); eventEmitter.on('event2', () => { setTimeout(() => { console.log('Second event here!'); eventEmitter.emit('event3'); }) }); eventEmitter.on('event3', () => { setTimeout(() => { console.log('Third event here!'); eventEmitter.emit('event1'); }) }); eventEmitter.emit('event1'); |
We explain the event loop a lot more in future parts of the series, but you can also check out When async is just not enough. An introduction to multithreading in the browser, where I explain this concept a bit in the context of comparing asynchronous functions and multithreading.
Handling events just once
When you register the listener using the “on” or “addEventListener” methods, the Node.js EventEmitter invokes it every time it emits the event.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 |
import * as EventEmitter from 'events'; class MyEventEmitter extends EventEmitter { counter = 0; } const eventEmitter = new MyEventEmitter(); eventEmitter.on('event', function () { console.log(this.counter++); }); eventEmitter.emit('event'); // 0 eventEmitter.emit('event'); // 1 eventEmitter.emit('event'); // 2 |
If instead of the “on” function you use “once,” you register a listener that the EventEmitter calls not more than once for a particular event.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 |
import * as EventEmitter from 'events'; class MyEventEmitter extends EventEmitter { counter = 0; } const eventEmitter = new MyEventEmitter(); eventEmitter.once('event', function () { console.log(this.counter++); }); eventEmitter.emit('event'); // 0 eventEmitter.emit('event'); // nothing happens eventEmitter.emit('event'); // nothing happens |
Summary
In this article, we went through the most essential features of the Node.js EventEmitter. It is due to the fact that it is widely used in other core features of Node.js. We encounter it as we go through other important concepts like the streams in upcoming parts of the series, so stay tuned!
Great Article👍👍