Node’s “Event Loop” is the core of its ability to handle large concurrency and high throughput. This is the most magical part, according to which Node.js can basically be understood as "single-threaded", while also allowing arbitrary operations to be processed in the background. This article will clarify how the event loop works so that you can feel its magic.
Event-driven programming
To understand the event loop, you must first understand Event Driven Programming. It appeared in 1960. Nowadays, event-driven programming is heavily used in UI programming. One of the main uses of JavaScript is to interact with the DOM, so using an event-based API is natural.
Simply defined: Event-driven programming controls the flow of an application through events or changes in state. Generally implemented through event monitoring, once the event is detected (ie, the state changes), the corresponding callback function is called. Sound familiar? In fact, this is the basic working principle of the Node.js event loop.
If you are familiar with client-side JavaScript development, think about those .on*() methods, such as element.onclick(), which are used to combine with DOM elements to deliver user interaction. This working mode allows multiple events to be triggered on a single instance. Node.js triggers this pattern through EventEmitters (event generators), such as in the server-side Socket and "http" modules. One or more state changes can be triggered from a single instance.
Another common pattern is to express success and fail. There are generally two common implementation methods. The first is to pass the "Error exception" into the callback, usually as the first parameter to the callback function. The second one uses the Promises design pattern and has added ES6. Note* Promise mode uses a function chain writing method similar to jQuery to avoid deep nesting of callback functions, such as:
The "fs" (filesystem) module mostly adopts the style of passing exceptions into callbacks. Technically triggering certain calls, such as the fs.readFile() attached event, but the API is just to alert the user and express the success or failure of the operation. The choice of such an API is based on architectural considerations rather than technical limitations.
A common misconception is that event emitters are also inherently asynchronous when firing events, but this is incorrect. Below is a simple code snippet to demonstrate this.
MyEmitter.prototype.doStuff = function doStuff() {
console.log('before')
emitter.emit('fire')
console.log('after')}
};
var me = new MyEmitter();
me.on('fire', function() {
console.log('emit fired');
});
me.doStuff();
// Output:
// before
// emit fired
// after
Note* If emitter.emit is asynchronous, the output should be
// before
// after
// emit fired
Mechanism overview and thread pool
Node itself relies on multiple libraries. One of them is libuv, the amazing library for handling asynchronous event queues and execution.
Node utilizes as much of the operating system kernel as possible to implement existing functions. Like generating response requests, forwarding connections and entrusting them to the system for processing. For example, incoming connections are queued through the operating system until they can be handled by Node.
You may have heard that Node has a thread pool, and you may wonder: "If Node will process tasks in order, why do we need a thread pool?" This is because in the kernel, not all tasks are processed in order. Executed asynchronously. In this case, Node.JS must be able to lock the thread for a certain period of time while operating so that it can continue executing the event loop without getting blocked.
The following is a simple example diagram to show its internal operating mechanism:
┌──────────────────────┐
╭──►│ timers timers
│ └───────────┬───────────┘
│ ┌───────────┴───────────┐
│ pending callbacks
│ └──────────┬───────────┘
|
│ │ │ POLL ││── Connections, │
│ since
│ ┌───────────┴───────────┐
╰─── ┤ setImmediate
└───────────────────────┘
There are some things that are difficult to understand about the internal workings of the event loop:
All callbacks will be preset via process.nextTick() at the end of one stage of the event loop (e.g., timer) and before transitioning to the next stage. This will avoid potential recursive calls to process.nextTick(), causing an infinite loop.
"Pending callbacks" are callbacks in the callback queue that will not be processed by any other event loop cycle (for example, passed to fs.write).
Simplify interaction with the event loop by creating an EventEmitter. It is a generic wrapper that allows you to create event-based APIs more easily. How the two interact often leaves developers confused.
The following example shows that forgetting that an event is triggered synchronously may cause the event to be missed.
function MyThing() {
EventEmitter.call(this);
doFirstThing();
this.emit('thing1');
}
util.inherits(MyThing, EventEmitter);
var mt = new MyThing();
mt.on('thing1', function onThing1() {
// Sorry, this event will never happen
});
function MyThing() {
EventEmitter.call(this);
doFirstThing();
setImmediate(emitThing1, this);
}
util.inherits(MyThing, EventEmitter);
function emitThing1(self) {
self.emit('thing1');
}
var mt = new MyThing();
mt.on('thing1', function onThing1() {
// Executed
});
The following solution will also work, but at the expense of some performance:
doFirstThing();
// Using Function#bind() will lose performance
setImmediate(this.emit.bind(this, 'thing1'));
}
util.inherits(MyThing, EventEmitter);
// Trigger error and handle it immediately (synchronously)
var er = doSecondThing();
if (er) {
This.emit('error', 'More bad stuff');
Return;
}
}
Conclusion
This article briefly discusses the inner workings and technical details of the event loop. It's all well thought out. Another article will discuss the interaction of the event loop with the system kernel and show the magic of NodeJS asynchronous operation.