Understanding Callbacks, Event Loops, and EventEmitters in Node
Callback
Node is built to be asynchronous in everything that it does. In this view, a callback is an asynchronous equivalent of a function that is called after a given task is completed. Alternatively, it can be defined as a function that is passed into another function so that the latter can call it on the completion of a given task. It allows other programs to keep running, thereby preventing blocking.
Let's consider a JavaScript global function, setTimeout(), as implemented in the following snippet:
setTimeout(function () { console.log("1…2…3…4…5 secs later."); }, 5000);setTimeout() accepts a callback function and a delay in milliseconds as first and second arguments, respectively. The callback function is fired after 5,000 milliseconds has elapsed, thereby printing "1…2…3…4…5 secs later." to the console.
An interesting thing is that the preceding code can be rewritten and simplified, as shown here:
var callback = function () { console.log("1…2…3…4…5 secs later."); }; setTimeout(callback, 5000)Another example of callbacks can be seen in filesystem operations. readFile (asynchronous) and readFileSync (synchronous) are two unique API functions in the Node library that can be used to read a file.
An example of synchronous reading is as follows:
var Filedata = fs.readFileSync('fileText.txt'); console.log(FileData);
The readFileSync() method reads a file synchronously (all of the content is read at the same time). It takes in the file path (this could be in the form of a string, URL, buffer, or integer) and an optional parameter (either an encoder, which could be a string, null, or a flag in the form of a string) as an argument. In the case of the preceding snippet, the synchronous filesystem function takes in a file (fileText.txt) from the directory and the next line prints the contents of the file as a buffer. Note that if the encoding option is specified, then this function returns a string. Otherwise, it returns a buffer, just as we've seen here.
An example of asynchronous reading is as follows:
var callback = function (err, FileData) { if (err) return console.error(err); console.log(FileData); }; fs.readFile('fileText.txt', callback);
In the preceding snippet, the readFile() method asynchronously reads the entire contents of a file (read serially until all its content is entirely read). It takes in the file path (this could be in the form of a string, URL, buffer, or integer), an optional parameter (either an encoder, which could be a string or null, or a flag in the form of a string), and a callback as arguments. It can be seen from the first line that the callback function accepts two arguments: err and data (FileData). The err argument is passed first because, as API calls are made, it becomes difficult to track an error, thus, it's best to check whether err has a value before you do anything else. If so, stop the execution of the callback and log the error. This is known as error-first callback.
In addition, if callbacks are not used (as seen in the previous example on synchronous reading) when dealing with a large file, you will be using massive amounts of memory, and this leads to a delay before the transfer of data begins (network latency). To summarize, from the use of the two filesystem functions, that is, the readFileSync(), which works synchronously, and the readFile(), which works asynchronously, we can deduce that the latter is safer than the former.
Also, in a situation where a callback is heavily nested (multiple asynchronous operations are serially executed), callback hell (also known as the pyramid of doom) may occur, when the code becomes unreadable and maintenance becomes difficult. Callback hell can be avoided by breaking callbacks into modular units (modularization) by using a generator with promises, implementing async/await, and by employing a control flow library.
Note
For more information on callback hell, refer to this link: http://callbackhell.com/.
Event Loops
An event loop is an efficient mechanism in Node that enables single-threaded execution. Everything that happens in Node is a reaction to an event. So, we can say that Node is an event-based platform, and that the event loop ensures that Node keeps running. Node uses event loops similar to a FIFO (first-in first-out) queue to arrange the tasks it has to do in its free time.
Let's assume that we have three programs running simultaneously. Given that each program is independent of another at any given time, they won't have to wait for the other before the output of each program is printed on the console. The following diagram shows the event loop cycle:
The following diagram shows the event queue and the call stack operations:
To understand the concept of an event loop, we will consider the following implementation of an event loop in a program to calculate a perimeter:
const perimeter = function(a,b){ return 2*(a+b); } constshowPerimeter = function(){ console.log(perimeter(2,2)); } const start = function(){ console.log('Initailizing.....'); console.log('Getting Started.....'); setTimeout(showPerimeter,10000); console.log('Ending program.....'); console.log('Exiting.....'); } start();The operation sequence of the preceding snippet is listed here:
start() is pushed to memory, which is known as a call stack.
console.log ('Initializing …..') is pushed and popped on execution.
console.log ('Getting Started …..') is pushed and popped on execution.
setTimeout(showPerimiter,10000) is pushed into the stack, which is where a timer is created by the API, and the program does not wait for the callback.
console.log ('Ending program…') is pushed and popped on execution.
If 10 seconds elapse, the showPerimiter(2,2) callback function will be sent to the event queue to wait until the stack is empty.
console.log ('Exiting program…') is pushed and popped on execution.
Executed.start() is also taken out of the stack.
The showPerimiter(2,2) function is moved to the stack and is executed.
Finally, 8 is printed to the command line, which is the perimeter.
Now, we are able to understand the concept of the event loop in Node. Next, we will explore the EventEmitter, which is one of the most important classes in Node.
EventEmitter
Callbacks are emitted and bound to an event by using a consistent interface, which is provided by a class known as EventEmitter. In real life, EventEmitter can be likened to anything that triggers an event for anyone to listen to.
EventEmitter implementation involves the following steps:
Importing and loading the event modules by invoking the "require" directive.
Creating the emitter class that extends the loaded event module.
Creating an instance of the emitter class.
Adding a listener to the instance.
Triggering the event.
EventEmitter Implementation
You can implement the EventEmitter by creating an EventEmitter instance using the following code:
var eEmitter = require('events'); // events module from node class emitter extends eEmitter {} // EventEmitter class extended var myeEmitter = new emitter(); // EventEmitter instance
The AddListener Method
EventEmitter makes it possible for you to add listeners to any random event. For flexibility, multiple callbacks can be added to a single event. Either addListener(event, listener) or on(event, listener) can be used to add a listener because they perform similar functions. You can use the following code block to add listeners:
var emitter = new MyClass(); emitter.on('event', function(arg1) { … });
You can also use the following code block to add listeners:
emitter.EventEmitter.addListener(event, function(arg1) { … });
Trigger Events
To trigger an event, you can use emit(event, arg1), as can be seen here:
EventEmitter.emit(event, arg1, arg2……)
The .emit function takes an unlimited number of arguments and passes them on to the callback(s) associated with the event.
By putting all of this code together, we have the following snippet:
var eEmitter = require('events'); class emitter extends eEmitter { } var myEemitter = new emitter(); myEemitter.on('event', () => { console.log('Hey, an event just occurred!'); }); myEemitter.emit('event');
Removing Listeners
We can also remove a listener from an event by using the removeListener(event, listener) or removeAllListeners(event) functions. This can be done using the following code:
EventEmitter. removeAllListeners (event, arg1, arg2……)
Alternatively, you can use the following code:
EventEmitter. removeListener(event, listener)
EventEmitter works synchronously; therefore, listeners are called in the order in which they are registered to ensure proper sequencing of events. This also helps avoid race conditions or logic errors. The setImmediate() or process.nextTick() methods make it possible for a listener to switch to an asynchronous mode.
For example, let's say we wanted an output such as "Mr. Pick Piper". We could use the following code:
console.log ("Mr.");console.log("Pick");console.log("Piper");
However, if we want an output such as "Mr. Piper Pick" using the preceding snippet, then we would introduce an event sequencing function, setImmediate(), to help the listener to switch to an asynchronous mode of operation, such as in the following code:
console.log ("Mr."); console.log("Pick"); setImmediate(function(){console.log("Piper");});
The output of the preceding function is exactly as expected:
Note
Callbacks, event loops, and event emitters are important concepts in Node.js. However, a more detailed description is beyond the scope of this book. For more information on these topics, please refer to this link: https://nodejs.org/.
Some Other Features of EventEmmitter
eventEmitter.once(): This can be used to add a callback that is expected to just trigger once, even when an event occurs repeatedly. It is very important to keep the number of listeners to a minimum (EventEmmitter expects the setMaxListeners method to be called). If more than a maximum of 10 listeners are added to an event, a warning will be flagged.
myEmitter.emit('error', new Error('whoops!'): This emits the typical action for an error event when errors occur within an EventEmmitter instance.