Node's "Event Loop" is the core of its ability to handle large concurrency and high throughput. This is the most magical place. According to this, Node.js can basically be understood as "single-threading", and it also allows arbitrary operations to be processed in the background. This article will illustrate how event loops work and you can feel its magic too.
Event-driven programming
To understand event loops, we must first understand event drive programming. It appeared in 1960. Today, event-driven programming is widely used in UI programming. One of the main uses of JavaScript is to interact with the DOM, so it is natural to use an event-based API.
Simply define: Event-driven programming controls the application process through changes in events or states. It is generally implemented through event monitoring. Once the event is detected (i.e., the state changes), the corresponding callback function is called. Sounds 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 pass user interaction. This working mode allows multiple events to be triggered on a single instance. Node.js triggers this mode through EventEmitter (event generator), such as in the Socket and "http" modules on the server side. One or more state changes may be triggered from a single instance.
Another common pattern is expressing success and failure. There are generally two common implementation methods now. The first thing is to pass the "Error exception" into the callback, which is generally passed to the callback function as the first parameter. The second type is to use the Promises design mode, and ES6 has been added. Note* Promise mode uses a jQuery-like function chain writing method to avoid deep callback function nesting, such as:
The code copy is as follows:
$.getJSON('/getUser').done(successHandler).fail(failHandler)
The "fs" (filesystem) modules mostly use the style of passing exceptions into the callback. Technically triggering certain calls, such as fs.readFile() attached event, but the API is only used to remind the user to express success or failure of the operation. Such an API is chosen for architectural reasons, not technical limitations.
A common misconception is that event emitters are also inherently asynchronous when triggering events, but this is incorrect. Here is a simple code snippet to prove this.
The code copy is as follows:
function MyEmitter() {
EventEmitter.call(this);
}
util.inherits(MyEmitter, EventEmitter);
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
EventEmitter is often manifested asynchronous because it is often used to notify operations that need to be completed asynchronously, but the EventEmitter API itself is fully synchronous. The listening function can be executed asynchronously, but please note that all listening functions will be executed synchronously in the order they are added.
Mechanism overview and thread pooling
Node itself relies on multiple libraries. One of them is libuv, a library that magically handles asynchronous event queues and executions.
Node uses as many existing functions as possible to utilize the operating system kernel. For example, a response request is generated, connections are forwarded and delegated to the system for processing. For example, incoming connections are queued through the operating system until they can be processed by Node.
You may have heard that Node has a thread pool, and you may wonder: "If Node handles tasks in order, why do you need a thread pool?" This is because in the kernel, not all tasks are executed asynchronously. In this case, Node.JS must be able to lock the thread for a period of time while it is operating so that it can continue to execute the event loop without being blocked.
The following is a simple example diagram to show its internal operating mechanism:
┌─────────────────────┐
──►│ timesers │
│ └─────────┬─────────┘
│ ┌────────────────────────────┐
│ │ pending callbacks │
│ └────────┬─────────────┘ ┌───────────────�
│ ┌───────────┴───────────┐ │ incoming: │
│ │ poll │◄──┤ connections, │
│ └─────────┬─────────┘ │ data, etc. │
│ ┌───────────────────────────────────────�
──┤ setImmediate │
└──────────────────┘
There are some difficulties in understanding the internal operation mechanism of the event loop:
All callbacks are preset via process.nextTick() before the end of one phase of the event loop (for example, a timer) and transition to the next phase. This will avoid the potential recursive call to process.nextTick(), causing an infinite loop.
"Pending callbacks" is a callback in the callback queue that will not be processed by any other event loop cycle (for example, passed to fs.write).
Event Emitter and Event Loop
By creating EventEmitter, interaction with event loops can be simplified. It is a universal encapsulation that makes it easier for you to create event-based APIs. How the two interact often makes developers feel confused.
The following example shows that forgetting that the event is triggered synchronously may cause the event to be missed.
The code copy is as follows:
// After v0.10, require('events').EventEmitter is no longer needed
var EventEmitter = require('events');
var util = require('util');
function MyThing() {
EventEmitter.call(this);
doFirstThing();
this.emit('thing1');
}
util.inherits(MyThing, EventEmitter);
var mt = new MyThing();
mt.on('thing1', function onThing1() {
// Sorry, this incident will never happen
});
The 'thing1' event above will never be caught by MyThing(), because MyThing() must be instantiated before it can listen for events. Here is a simple workaround without having to add any extra closures:
The code copy is as follows:
var EventEmitter = require('events');
var util = require('util');
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() {
// Execute
});
The following scheme can also work, but some performance is lost:
The code copy is as follows:
function MyThing() {
EventEmitter.call(this);
doFirstThing();
// Using Function#bind() will lose performance
setImmediate(this.emit.bind(this, 'thing1'));
}
util.inherits(MyThing, EventEmitter);
Another problem is triggering an Error (Exception). It's already hard to find out the problem in your application, but without the call stack (note *e.stack), debugging is almost impossible. The call stack will be lost when the Error is requested by the remote asynchronously. There are two possible solutions: synchronous triggering or ensuring that Error is passed in with other important information. The following example demonstrates these two solutions:
The code copy is as follows:
MyThing.prototype.foo = function foo() {
// This error will be triggered asynchronously
var er = doFirstThing();
if (er) {
// When triggering, you need to create a new error that retains the on-site call stack information.
setImmediate(emitError, this, new Error('Bad stuff'));
return;
}
// Trigger error and process it immediately (synchronize)
var er = doSecondThing();
if (er) {
this.emit('error', 'More bad stuff');
return;
}
}
Assess the situation. When an error is triggered, it is possible to be processed immediately. Alternatively, it may be trivial and can be easily handled, or handled later. In addition, passing an Error through a constructor is not a good idea, because the constructed object instance is likely to be incomplete. The exception is the case where the Error was directly thrown just now.
Conclusion
This article briefly explores the internal operating mechanism and technical details of the event loop. All are carefully considered. Another article will discuss the interaction between event loops and system kernels and demonstrate the magic of NodeJS running asynchronously.