- 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
JavaScript and the Node.js environment uses the Event Loop. In this article, we explore the idea of it and go through different phases that the event loop has. Although this series aims to present the usage of Node.js with TypeScript, this time we don’t get much of a chance to do it. In this part of the course, we cover setTimeout, setInterval, setImmediate, and process.nextTick functions. To do it, we briefly go through different phases of the event loop to illustrate the differences between the above functions.
The event loop in Node.js
The event loop is a mechanism that browsers also implement, but in this article, we focus on the implementation that Node.js environment uses, done by the libuv library. It was designed for use in Node.js, but now it is a separate project.
Node.js constantly executes the event loop, and every iteration of it we call a tick. If during an iteration events wait in the queue, they are invoked one by one. Node.js initializes the event loop on start, and each iteration has a set of phases defining an order of operations. Every phase has its queue of functions to execute. There are many opportunities for an event to be put into the event loop: some of the best examples are the setTimeout function and callbacks for the Ajax requests. Turns out, they have their separate place in the Event Loop. Let’s dig into it!
Timers
The first phase is the timers. During that phase, the event loop executes callbacks scheduled by the setTimeout and setInterval. Since we often use the above functions, let’s look into them more.
setTimeout
The simple way of looking into the setTimeout would be to state that it sets a timer that executes a function once the timer expires. The above is too much of an oversimplification. The setTimeout function specifies a timer with a threshold after which the provided callback should be executed rather than the exact time. To understand it, we need to take into consideration how the event loop works.
1 2 3 4 5 6 7 8 9 |
console.time('setTimeout'); setTimeout(() => { console.log('Timer went off'); console.timeEnd('setTimeout'); }, 100); setTimeout(() => { for(let i = 0; i < 10000000; ++i); }, 95); |
In the example above we call setTimeout twice. The first time, we measure the time between calling the setTimeout function and the callback. It turns out that it is over 100ms. This because when we call the setTimeout function, it sets up a timer after which it pushes the callback to the timers phase queue. The issue here is that when that happens, there is another function running, and therefore the next function in the queue needs to wait.
setInterval
The setInterval works similarly, but it causes the function to execute repeatedly with a certain time delay between each call. It returns an id that we can use to stop the execution.
1 2 3 4 5 6 |
let i = 0; const id = setInterval(() => { console.log(++i); if(i > 10) clearInterval(id); }, 50); |
The intriguing part is that we can do a similar thing using the setTimeout function:
1 2 3 4 5 6 7 8 9 10 |
let i = 0; function increment() { console.log(++i); if(i <= 10) { setTimeout(increment, 50); } } increment(); |
The setInterval delays the function regularly regardless of the state of the previous function call. If setInterval is timed to deliver every 1000ms and the execution takes 500ms, the actual interval between the end of the call and the next invocation is 500ms.
The recursive setTimeout, on the other hand, schedules a new function call when the previous one ends. The longer the previous function took to complete, the bigger the interval between those two functions starting.
Pending callbacks, idle, and prepare
After the timers phase, there go a few of less crucial phases for us, beginning with the pending callbacks phase. It executes callbacks for some systems operations, such as TCP errors. The next phases we call idle and prepare, but the Node.js only uses them internally. If you want to dig deeper, you can read through the introduction to libuv.
Poll
Input/Output related callbacks execute during the poll phase – for example, the ones connected to the File System module. If there are any events, they are handled one by one. When that’s not the case, Node.js waits a bit for new events to come up here and execute them immediately. If any scripts have been scheduled using the setImmediate function, the Node.js does not wait and goes straight to the check phase.
Check
The check phase runs immediately after the poll phase and invokes callbacks set up using the setImmediate function. It is designed to execute a script once the current poll phase completes. It is a bit more performant than using setTimeout(() => {}, 0) , because it does not have to start a timer of any sort.
Let’s try to run both of them and compare the results:
1 2 3 4 5 6 |
setTimeout(() => { console.log('set timeout'); }, 0); setImmediate(() => { console.log('set immediate'); }); |
If we run the above code multiple times, we can observe that it is not predictable which one of the callbacks runs first. There is a situation in which the setImmediate function might be considered to have an advantage. If we run both setTimeout and setImmediate within the I/O cycle (for example in a callback after reading a file), the latter is always called first:
1 2 3 4 5 6 7 8 9 10 |
import * as fs from 'fs'; fs.readFile(__filename, () => { setTimeout(() => { console.log('set timeout'); }, 0); setImmediate(() => { console.log('set immediate'); }); }); |
set immediate
set timeout
The above behavior is due to the fact, that the fs.readFile callback runs in the poll phase. The next phase after that is check, so if we attach some callbacks to it using the setImmediate function, it is guaranteed to be first.
Close callbacks
In this phase, the close callbacks run. An example of that is the ‘close‘ event emitted when a socket is closed using the socket.destroy() function.
If you would like to know more about websockets, check out Introduction to WebSockets. Creating a Node.js server and using WebSocket API in the browser
process.nextTick
Another similar function is the process.nextTick. It is not mentioned in the diagram at the top of the article, because it is not technically part of the event loop. When we look into the documentation, we can see that the process.nextTick fires immediately on the same phase that the event loop currently is. It means that it invokes more immediately than setImmediate and even the official documentation acknowledges that those functions should be swapped. This is not going to happen though because it would break a lot of existing code.
Summary
In this article, we went through a lot of different ways to interact with the event loop. We used the setTimeout and setInterval functions that make use of timers and described differences between them. We also covered the setImmediate function that sets up callbacks for the Check phase, and how it interferes with the Poll phase. This, along with the knowledge of how the process.nextTick works can give you an idea of differences between those functions and how does the event loop works. Although when discussing the above functions, we briefly went through different phases in the event loop, I encourage you to check out the official documentation for details.