- Mastering Node.js
- Sandro Pasquali
- 898字
- 2021-07-21 18:17:14
Understanding the event loop
Node processes JavaScript instructions using a single thread. Within your JavaScript program no two operations will ever execute at exactly the same moment, as might happen in a multithreaded environment. Understanding this fact is essential to understanding how a Node program, or process, is designed and runs.
This does not mean that only one thread is being used on the machine hosting this a Node process. Simply writing a callback does not magically create parallelism! Recall Chapter 1, Understanding the Node Environment, and our discussion about the process
object—Node's "single thread" simplicity is in fact an abstraction created for the benefit of developers. It is nevertheless crucial to remember that there are many threads running in the background managing I/O (and other things), and these threads unpredictably insert instructions, originally packaged as callbacks, into the single JavaScript thread for processing.
Node executes instructions one by one until there are no further instructions to execute, no more input or output to stream, and no further callbacks waiting to be handled.
Even deferred events (such as timeouts) require an eventual interrupt in the event loop to fulfill their promise.
For example, the following while
loop will never terminate:
var stop = false; setTimeout(function() { stop = true; }, 1000); while(stop === false) {};
Even though one might expect, in approximately one second, the assignment of a Boolean true
to the variable stop
, tripping the while
conditional and interrupting its loop, this will never happen. Why? This while
loop starves the event loop by running infinitely, greedily checking and rechecking a value that is never given a chance to change, as the event loop is never given a chance to schedule our timer callback for execution.
As such, programming Node implies programming the event loop. We've previously discussed the event sources that are queued and otherwise arranged and ordered on this event loop—I/O events, timer events, and so on.
When writing non-deterministic code it is imperative that no assumptions about eventual callback orders are made. The abstraction that is Node masks the complexity of the thread pool on which the straightforward main JavaScript thread floats, leading to some surprising results.
We will now refine this general understanding with more information about how, precisely, the callback execution order for each of these types is determined within Node's event loop.
Four sources of truth
We have learned about the four main groups of deferred event sources, whose position and priority on the stack we will now demonstrate:
- Execution blocks: The blocks of JavaScript code comprising the Node program, being expressions, loops, functions, and so on. This includes
EventEmitter
events emitted within the current execution context. - Timers: Callbacks deferred to sometime in the future specified in milliseconds, such as
setTimeout
andsetInterval
. - I/O: Prepared callbacks returned to the main thread after being delegated to Node's managed thread pool, such as filesystem calls and network listeners.
- Deferred execution blocks: Mainly the functions slotted on the stack according to the rules of
setImmediate
andnextTick
.
We have learned how the deferred execution method setImmediate
slots its callbacks after I/O callbacks in the event queue, and nextTick
slots its callbacks before I/O and timer callbacks.
var fs = require('fs'); var EventEmitter = require('events').EventEmitter; var pos = 0; var messenger = new EventEmitter(); // Listener for EventEmitter messenger.on("message", function(msg) { console.log(++pos + " MESSAGE: " + msg); }); // (A) FIRST console.log(++pos + " FIRST"); // (B) NEXT process.nextTick(function() { console.log(++pos + " NEXT") }) // (C) QUICK TIMER setTimeout(function() { console.log(++pos + " QUICK TIMER") }, 0) // (D) LONG TIMER setTimeout(function() { console.log(++pos + " LONG TIMER") }, 10) // (E) IMMEDIATE setImmediate(function() { console.log(++pos + " IMMEDIATE") }) // (F) MESSAGE HELLO! messenger.emit("message", "Hello!"); // (G) FIRST STAT fs.stat(__filename, function() { console.log(++pos + " FIRST STAT"); }); // (H) LAST STAT fs.stat(__filename, function() { console.log(++pos + " LAST STAT"); }); // (I) LAST console.log(++pos + " LAST");
The output of is program is:
- FIRST (A).
- MESSAGE: Hello! (F).
- LAST (I).
- NEXT (B).
- QUICK TIMER (C).
- FIRST STAT (G).
- LAST STAT (H).
- IMMEDIATE (E).
- LONG TIMER (D).
Let's break the preceding code down:
A, F, and I execute in the main program flow and as such they will have the first priority in the main thread (this is obvious; your JavaScript executes its instructions in the order they are written, including the synchronous execution of the emit
callback).
With the main call stack exhausted, the event loop is now almost reading to process I/O operations. This is the moment when nextTick requests are honored slotting in at the head of the event queue. This is when B is displayed.
The rest of the order should be clear. Timers and I/O operations will be processed next, (C, G, H) followed by the results of the setImmediate
callback (E), always arriving after any I/O and timer responses are executed.
Finally, the long timeout (D) arrives, being a relatively far-future event.
Notice that re-ordering the expressions in this program will not change the output order (outside of possible re-ordering of the STAT results, which only implies that they have been returned from the thread pool in different order, remaining as a group in the correct order as relates to the event queue).
- ASP.NET Core:Cloud-ready,Enterprise Web Application Development
- Cocos2D-X權威指南(第2版)
- 深入實踐Spring Boot
- 單片機應用技術
- The DevOps 2.4 Toolkit
- Data Analysis with Stata
- 碼上行動:用ChatGPT學會Python編程
- 可解釋機器學習:模型、方法與實踐
- Learning Python Data Visualization
- Visual Basic程序設計全程指南
- 數據科學中的實用統計學(第2版)
- Python應用與實戰
- Elasticsearch Blueprints
- Mastering OpenStack
- Mastering Machine Learning with scikit-learn