Event Loop in Node.js

Posted by Matty999555 on Fri, 19 Jul 2019 08:22:48 +0200

What is Event Cycle

As we all know, JavaScript is single-threaded, and Nodejs can implement non-blocking I/O operations, because Event Loop exists.

Event Loop has the following stages, a rectangle represents a stage, as follows:

   ┌───────────────────────┐
┌─>│        timers         │
│  └──────────┬────────────┘
│  ┌──────────┴────────────┐
│  │     I/O callbacks     │
│  └──────────┬────────────┘
│  ┌──────────┴────────────┐
│  │     idle, prepare     │
│  └──────────┬────────────┘      ┌───────────────┐
│  ┌──────────┴────────────┐      │   incoming:   │
│  │         poll          │<─────┤  connections, │
│  └──────────┬────────────┘      │   data, etc.  │
│  ┌──────────┴────────────┐      └───────────────┘
│  │        check          │
│  └──────────┬────────────┘
│  ┌──────────┴────────────┐
└──┤    close callbacks    │
   └───────────────────────┘

Each stage maintains a FIFO queue structure. When the event loop enters a certain stage, some specific operations of that stage are performed, and then the callback functions in the queue are executed sequentially. When the callback functions in the queue are executed or the number of callback functions that have been executed reaches a certain maximum, the next stage will be entered.

timers

At the beginning of the event loop, the callback functions of setTimeout and setInterval are executed.

When the timer's time is up, the callback function of the timer is put into the queue and executed sequentially.

If you have four timers a, b and c, the time intervals are 10 ms, 20 ms and 30 ms, respectively. When we enter the timer phase of the event cycle, the time passes by 25 ms, then the callbacks of timers A and b will be executed and the next phase will be entered after execution.

I/O callbacks

Execute callback functions other than setTimeout, setInterval, setImmediate, and close callbacks.

idle, prepare

Do some internal operations.

poll

This should be the most important stage in the event cycle.

If the queue at this stage is not empty, the callbacks in the queue will be executed sequentially; if the queue is empty, the setImmediate function will also be called, then the check ing stage will be entered. If the queue is empty and there is no setImmediate function call, the event loop waits and executes as soon as a callback function is added to the queue.

check

setImmediate callbacks are executed at this stage.

close callbacks

Callbacks such as socket.on('close',...) are executed at this stage.

setTimout vs setImmediate

// timeout_vs_immediate_1.js
setTimeout(function timeout() {
  console.log('timeout');
}, 0);

setImmediate(function immediate() {
  console.log('immediate');
});

According to the previous statement, the event loop enters the timer stage first, and the setTimeout callback is executed until the check ing stage, before the setImmediate callback is executed. So some people think that the output of the above code should be:

$ node timeout_vs_immediate_1.js
timeout
immediate

But the results here are uncertain. The performance of the follow-up process is often related here, and the setTimeout interval here, though zero, will actually be 1. So when the starter enters the event loop and the time has not passed 1ms, the queue in the timer phase is empty and no callbacks are executed. There is also a call to the setImmediate function, so the callback to the setImmediate will be called when it comes to the check stage. If the event loop has consumed 1 ms when it enters the timer phase, then the setTimeout callback will be executed at this time, before entering the check phase, and then the setImmediate callback will be executed.

Therefore, the following two kinds of output may occur.

$ node timeout_vs_immediate_1.js
timeout
immediate

$ node timeout_vs_immediate_1.js
immediate
timeout

Suppose the above code is placed in an I/0 loop, such as

// timeout_vs_immediate_2.js
const fs = require('fs');

fs.readFile(__filename, () => {
  setTimeout(() => {
    console.log('timeout');
  }, 0);
  setImmediate(() => {
    console.log('immediate');
  });
});

So the result is definite. The output is as follows.

$ node timeout_vs_immediate_2.js
immediate
timeout

process.nextTick()

process.nextTick() is not part of the event loop, but is also an asynchronous API.

After the current operation is completed, if there is a call to process.nextTick(), then the callback in process.nextTick() will be executed. If there is process.nextTick() in the callback, then the callback of process.nextTick() in the callback will also be executed. So procee.nextTick() may block the event cycle to the next stage.

// process_nexttick_1.js
let i = 0;

function foo() {
    i += 1;

    if (i > 3) return;

    console.log('foo func');

    setTimeout(() => {
        console.log('timeout');
    }, 0);

    process.nextTick(foo);
}

setTimeout(foo, 5);

According to the previous statement, the above output results are as follows:

$ node process_nexttick_1.js
foo func
foo func
foo func
timeout
timeout
timeout

You may wonder whether process.nextTick() is executed after the queues at some stage of the event cycle are empty or after a callback in the queue is executed. Think about the output of the following code?

// process_nexttick_2.js
let i = 0;

function foo() {
    i += 1;

    if (i > 2) return;

    console.log('foo func');

    setTimeout(() => {
        console.log('timeout');
    }, 0);

    process.nextTick(foo);
}

setTimeout(foo, 2);
setTimeout(() => {
    console.log('another timeout');
}, 2);

Execute and see the results.

// node version: v11.12.0
$ node process_nexttick_2.js
foo func
foo func
another timeout
timeout
timeout

As shown above, process.nextTick() is executed after a callback in the queue is completed.

As you can see above, there is a comment node version: v11.12.0. That is, the node version running this code is 11.12.0. If your node version is lower than this, such as 7.10.1, you may get different results.

// node version: v7.10.0
$ node process_nexttick_2.js
foo func
another timeout
foo func
timeout
timeout

Different versions behave differently. I think the new version has been updated and adjusted.

process.nextTick() vs Promise

process.nextTick() corresponds to nextTickQueue, and Promise corresponds to microTaskQueue.

Neither of them belongs to a part of the event cycle, but when they are executed after the current operation, how about the execution of the two?

// process_nexttick_vs_promise.js
let i = 0;

function foo() {
    i += 1;

    if (i > 2) return;

    console.log('foo func');

    setTimeout(() => {
        console.log('timeout');
    }, 0);

    Promise.resolve().then(foo);
}

setTimeout(foo, 0);

Promise.resolve().then(() => {
    console.log('promise');
});

process.nextTick(() => {
    console.log('nexttick');
});

Running the code, the results are as follows:

$ node process_nexttick_vs_promise.js
nexttick
promise
foo func
foo func
timeout
timeout

If you understand the output results above, you can also understand the event loop in Nodejs.

Reference material

discuss

Original address

Welcome to discuss with us. If you have any good points, please correct them.

Topics: node.js Javascript socket