Login With Github

Node Timer Tutorial In 20 Minutes

Operations in JavaScript are single-threaded, so asynchronous operations are more important.

As long as you use the functions outside the engine, you need to interact with the outside, so the asynchronous operation is needed. Because there are so many asynchronous operations, JavaScript provides a lot of asynchronous syntax.

The asynchronous syntax is more complicated than the browser, because it can communicate with the kernel, so there is a special library libuv for it. The library is responsible for the execution time of various callback functions, as at the end asynchronous tasks need to return to the main thread to queue for execution.

To coordinate asynchronous tasks, Node provides four timers that allow tasks to run at a specified time.

  • setTimeout()
  • setInterval()
  • setImmediate()
  • process.nextTick()

The first two are the standards for the javascript language, while the latter two are unique to Node.

Can you tell the result of the following code?

// test.js
setTimeout(() => console.log(1));
setImmediate(() => console.log(2));
process.nextTick(() => console.log(3));
Promise.resolve().then(() => console.log(4));
(() => console.log(5))();

The results are as follows.

$ node test.js
5
3
4
1
2

If your answer is right, you probably don't need to read the article any more, as this article mainly explains how Node handles various timers, or more broadly, how the libuv library schedules and executes asynchronous tasks on the main thread.

1. Synchronous tasks and asynchronous tasks

Synchronous tasks are always executed earlier than asynchronous tasks.

In the previous code, only the last line is a synchronous task, so it is executed at the earliest.

(() => console.log(5))();

2. The current round loop and the next round loop

Asynchronous tasks can be divided into two types.

  • asynchronous tasks appended in the current round loop
  • asynchronous tasks appended in the next round loop

The loop here refers to the event loop. It is the way how the JavaScript engine handles asynchronous tasks, which will be explained more later. What we should know here is that the current round loop must be executed earlier than the next round loop.

Node specifies that the callback functions of process.nextTick and Promise are appended to the current round loops, so they will be executed once the synchronized tasks finish being executed. And the callback functions of setTimeout, setInterval, and setImmediate are appended to the next round loop.

So the third and fourth lines of the first piece of code in the article are executed earlier than the first and second lines.

// The following two lines of code are executed in the next round loop.
setTimeout(() => console.log(1));
setImmediate(() => console.log(2));
// The following two lines of code are executed in the current round loop.
process.nextTick(() => console.log(3));
Promise.resolve().then(() => console.log(4));

3. process.nextTick()

The name of process.nextTick is a bit misleading. It is executed in the current round loop and is executed earliest among all of the asynchronous tasks.

Node executes the task queue of process.nextTick after finishing all synchronization tasks. So, the second output comes from the following line of code.

process.nextTick(() => console.log(3));

Generally, if you want asynchronous tasks to be executed as quickly as possible, use process.nextTick.

4. Microtasks

According to the language specification, the callback function of the Promise object will go into the queue of the microtasks in the asynchronous tasks.

The microtask queue is appended to the process.nextTick queue and belongs to the current round of loops. Therefore, the following code always outputs 3 first and then 4.

process.nextTick(() => console.log(3));
Promise.resolve().then(() => console.log(4));
// 3
// 4

Note that the next queue will be executed only after the previous queue is emptied completely.

process.nextTick(() => console.log(1));
Promise.resolve().then(() => console.log(2));
process.nextTick(() => console.log(3));
Promise.resolve().then(() => console.log(4));
// 1
// 3
// 2
// 4

In the above code, all the callbacks of process.nextTick will be executed earlier than the callbacks of Promise.

The execution sequence inside the current round loop is as follows.

  1. synchronization tasks
  2. process.nextTick()
  3. microtasks

5. The event loop

Before getting to introduce the execution sequence of the next round of loops, it's necessary to understand what an event loop is.

Here is what the official documentation of Node says.

"When Node.js starts, it initializes the event loop, processes the provided input script which may make async API calls, schedule timers, or call process.nextTick(), then begins processing the event loop."

It means that:

  • Some may think that there is a separate event loop thread in addition to the main thread. In fact, there is only a main thread, and the event loop is completed on the main thread.
  • Second, when Node starts executing the script, it will initialize the event loop first. At this time the event loop has not yet started, and the following things will be completed first.
  1. Synchronization tasks
  2. Making an asynchronous request
  3. Planning the time when the timer takes effect
  4. Executing process.nextTick(), etc.
  • The event loop starts after all these things have been done.

6. Six phases of the event loop

The event loop will be executed indefinitely, round after round. It stops executing only after the callback function queue for the asynchronous task is emptied.

Each round of the event loop is divided into six phases. The execution order of these phases is as follows.

  1. timers
  2. I/O callbacks
  3. idle, prepare
  4. poll
  5. check
  6. close callbacks

There is a FIFO callback function queue for each stage. Only when the callback function queue of one stage has been emptied, does the event loop move on to the next stage.

I'll give a brief introduction to each stage. And you can also refer to the official documentation, or the source code interpretation of libuv for more details.

(1)timers

This is the timer phase, which handles the callback functions of setTimeout() and setInterval(). After entering this phase, the main thread will check whether the current time satisfies the conditions of the timer. If it satisfies, it will execute the callback function, otherwise leave this stage.

(2) I/O callbacks

The other callbacks are executed in this phase except for the callback functions for the following operations.

  • The callback functions of setTimeout() and setInterval()
  • The callback function of setImmediate()
  • The callback functions used to close the request, such as socket.on('close', ...)

(3) idle, prepare

This phase is only for the internal calls of libuv and can be ignored here.

(4) Poll

This stage is for the polling, which is used to wait for the I/O events that have not yet been returned, such as server responses, users' moving the mouse, and so on.

The time for this stage will be longer. If there are no other asynchronous tasks to process (such as expired timers), it will stay at this stage and wait for the I/O requests to return results.

(5) check

This stage executes the callback function of setImmediate().

(6) close callbacks

This stage executes the callback function for closing the request, such as socket.on('close', ...).

7. An example of an event loop

Below is an example from the official documentation.

const fs = require('fs');

const timeoutScheduled = Date.now();

// Asynchronous task 1: Timer executed after 100ms
setTimeout(() => {
  const delay = Date.now() - timeoutScheduled;
  console.log(`${delay}ms`);
}, 100);

// Asynchronous task 2: After reading the file, there is a callback function that takes 200ms.
fs.readFile('test.js', () => {
  const startCallback = Date.now();
  while (Date.now() - startCallback < 200) {
    // Do nothing
  }
});

In the above code, there are two asynchronous tasks: one is a timer executed after 100ms; and the other is reading the file, and its callback function takes 200ms. What is the result then?

After the script enters the first round of the event loop, there are no expired timers and I/O callback functions that can be executed, so it will enter the Poll stage and wait for the kernel to return the result of reading the file. Since it usually does not exceed 100ms for reading small files, the Poll stage can get the results before the timer expires, and then it moves on executing.

In the second round of the event loop, there are still no expired timers, but there are already I/O callback functions that can be executed, so it will enter the I/O callbacks phase and execute the callback function of fs.readFile. This callback function takes 200ms, so when it is halfway through, the timer which takes 100ms will expire. However, only the callback function finishes being executed can it leave this stage.

In the third round of the event loop, there are already expired timers, so the timers will be executed during the timers phase. And the time for the final output is about more than 200 ms.

8. setTimeout and setImmediate

Since setTimeout is executed during the timers phase, and setImmediate is executed during the check phase, setTimeout will be completed earlier than setImmediate.

setTimeout(() => console.log(1));
setImmediate(() => console.log(2));

The above code should output 1 first, and then output 2. However, actually, when it is executed, the result is uncertain: Sometimes it outputs 2 first, and then outputs 1.

This is because the second parameter of setTimeout defaults to 0. But in fact, 0 ms is impossible in Node, and it needs 1 ms at least. According to the official documentation, the second parameter ranges from 1 ms to 2147483647 ms. So setTimeout(f, 0) is equivalent to setTimeout(f, 1).

In the actual execution, after entering the event loop, it may be more than 1 ms or less than 1 ms, which depends on the current situation of the system. If it is less than 1 ms, the timers phase will be skipped and it will enter the check phase to execute the callback function of setImmediate first.

However, the following code must output 2 first, and then output 1.

const fs = require('fs');

fs.readFile('test.js', () => {
  setTimeout(() => console.log(1));
  setImmediate(() => console.log(2));
});

In the above code, it first go into the I / O callbacks phase, then the check phase, and finally the timers phase. Therefore, setImmediate is executed earlier than setTimeout.

9. Reference

0 Comment

temp