This article is published under a Signature 4.0 International (CC BY 4.0) license. Signature 4.0 International (CC BY 4.0)

🌟🌟🌟🌟🌟

Taste: Foie gras

Cooking time: 20min

Event loop


As can be seen from the figure, each event cycle consists of the six phases shown in the figure above, which we will explain one by one.

Timers timer

Timers fall into two categories:

  • Immediate Executes in the next check phase
  • Timeout Execution after the timer expires (default delay value: 1ms)

There are two types of Timeout timers:

  • Interval
  • Timeout

This phase executes the callbacks set by setTimeout() and setInterval()

The execution of Timers is controlled by the poll phase

SetTimeout () and setInterval() are the same apis as in the browser. They work similarly to asynchronous I/O, but do not require the participation of an I/O thread pool.

Once created, the two timers are inserted into a red-black tree inside the timer observer. Each time the Tick is executed, timer objects are removed from the red-black tree to check if they have exceeded the timer time, and then their callback is executed.

Note: One problem with timers is that they are not absolutely accurate (within tolerance). If a task in an event loop takes too much time, the time will be affected when the timer is executed again.

None I/O processing

setTimeout(function timeout () {
    console.log('timeout');
},0);
setImmediate(function immediate () {
    console.log('immediate');
});
Copy the code

By executing the code above, we can see that the output is inconclusive.

Because setTimeout (fn, 0) has a few milliseconds of uncertainty, there is no guarantee that the timers will execute the handlers immediately after entering the timers phase.

There are I/O processing cases

var fs = require('fs');
fs.readFile(__filename, () => {
    setTimeout((a)= > {
        console.log('timeout');
    }, 0);
    setImmediate((a)= > {
        console.log('immediate');
    });
})
// immediate
// timeout
Copy the code

SetImmediate takes priority over setTimeout because the Poll phase is completed and then the Check phase, while timers phase is the next event loop phase.

Pending Callbacks pending callbacks

Perform partial callbacks, except for callbacks set by close, Times, and setImmediate()

The system-level callback queue that will be executed in the next loop, such as TCP error catching, etc

Idle, perpare

For internal use only

Poll polling

Gets a new I/O event, where Node.js blocks under appropriate conditions

The main tasks of this phase are to perform callbacks to timers that have reached the delay time and to process events in the poll queue.

When the event loop enters the POLL phase and the timer is not called, two things happen:

1. If the poll queue is not empty, the event loop will traverse the callback queue that synchronously executes them.

2. If the poll queue is empty, it is divided into two cases:

  • If called by the setImmediate() callback, the event loop ends the poll phase and goes to the Check phase.

  • If not called by the setImmediate() callback, the event loop blocks and waits for the callback to be added to the poll queue to execute.

Once the poll queue is empty, the event loop checks to see if the timers have reached the delay time, and if one or more timers have reached the delay time, the event loop rolls back to the Timers stage to perform their callbacks.

Check test

The callback set by setImmediate() executes during this phase

As in the second case of the poll phase above, if the poll queue is empty and is called by the setImmediate() callback, the event loop goes directly to the Check phase.

Close Callbacks A closed callback function

The socket.on('close',callback) callback is executed at this stage

libuv

Libuv provides the entire event loop functionality for Node.js.


As shown in the figure above, Event loops are created based on IOCP under Windows, epoll under Linux, KQueue under FreeBSD, and Event Ports under Solaris.

Taking a closer look at the figure above, Network I/O and File I/O, DNS and other implementations are separated because they are essentially implemented by two sets of mechanisms. We’ll peek into their essence in the source code in a moment.

Essentially, when we write JavaScript code to call the core module of Node, the core module calls the C++ built-in module, which makes system calls through libuv.

Libuv mainly solves problems

In the real world, it is very difficult to support different types of I/O on all different types of operating system platforms. In order to support cross-platform I/O and better manage the whole process, liBUV was abstracted.

Libuv abstracts a layer of APIS that allows you to call various system features on various platforms and machines, including manipulating files, listening on sockets, and so on, without needing to know their implementation.

Core source code interpretation

The core function uv_run

The source code

int uv_run(uv_loop_t* loop, uv_run_mode mode) {
    int timeout;
    int r;
    int ran_pending;
    // Check if there are asynchronous tasks in the loop.
    r = uv__loop_alive(loop);
    if(! r) uv__update_time(loop);// Event loop while
    while(r ! =0 && loop->stop_flag == 0) {
        // Update the event phase
        uv__update_time(loop);  
        // Handle the timer callback
        uv__run_timers(loop);        
        // Handle asynchronous task callbacks
        ran_pending = uv__run_pending(loop);      
        // For internal use
        uv__run_idle(loop);
        uv__run_prepare(loop);        
        // uv_backend_timeout is passed to uv__io_poll after the calculation is complete
        // If timeout = 0, uv__io_poll will be skipped
        timeout = 0;
        if((mode == UV_RUN_ONCE && ! ran_pending || mode == UV_RUN_DEFAULT)) timeout = uv_backend_timeout(loop); uv__io_poll(loop, timeout);/ / check phase
        uv__run_check(loop);
        // Close operations such as file descriptors
        uv__run_closing_handles(loop);
        // Check if there are asynchronous tasks in the loop.
        r = uv__loop_alive(loop);
        if (mode == UV_RUN_ONCE || mode == UV_RUN_NOWAIT)
          break;
    }
    return r;
}
Copy the code

The true nature of the event loop is a while.

Network I/O, File I/O and DNS are implemented by two sets of mechanisms.

The last call to Network I/O comes down to a function called uv__io_start, which puts the REQUIRED I/O events and callbacks into the Watcher queue. The Uv__io_poll phase takes the events from the Watcher queue and calls the system interface for execution.

(uv__io_poll part of the code is too long, you can see it yourself)

uv__io_start

void uv__io_start(uv_loop_t* loop, uv__io_t* w, unsigned int events) {
  assert(0 == (events & ~(POLLIN | POLLOUT | UV__POLLRDHUP | UV__POLLPRI)));
  assert(0! = events); assert(w->fd >=0);
  assert(w->fd < INT_MAX);
  w->pevents |= events;
  maybe_resize(loop, w->fd + 1);
  if (w->events == w->pevents)
    return;
  if (QUEUE_EMPTY(&w->watcher_queue))
    QUEUE_INSERT_TAIL(&loop->watcher_queue, &w->watcher_queue);
  if (loop->watchers[w->fd] == NULL) { loop->watchers[w->fd] = w; loop->nfds++; }}Copy the code

As shown above is the main line implementation process of Network I/O in libuv.

The other main line is that operations such as Fs I/O and DNS call uv__work_sumit, which is the function that performs the final call in the thread pool initialization uv_queue_work.

void uv__work_submit(uv_loop_t* loop,
                     struct uv__work* w,
                     enum uv__work_kind kind,
                     void (*work)(struct uv__work* w),
                     void (*done)(struct uv__work* w, int status)) {
  uv_once(&once, init_once);
  w->loop = loop;
  w->work = work;
  w->done = done;
  post(&w->wq, kind);
}
Copy the code
int uv_queue_work(uv_loop_t* loop,
                  uv_work_t* req,
                  uv_work_cb work_cb,
                  uv_after_work_cb after_work_cb) {
  if (work_cb == NULL)
    return UV_EINVAL;
  uv__req_init(loop, req, UV_WORK);
  req->loop = loop;
  req->work_cb = work_cb;
  req->after_work_cb = after_work_cb;
  uv__work_submit(loop,
                  &req->work_req,
                  UV__WORK_CPU,
                  uv__queue_work,
                  uv__queue_done);
  return 0;
}
Copy the code

Event queues in Node.js

Node.js has multiple queues in which different types of events are queued. At the end of one phase, the event loop processes the intermediate queue in the middle before moving on to the next.

There are four main types of queues in the native Libuv event loop:

  • Expired timer and interval queues

  • IO event queue

  • Immediates queue

  • The close handlers queue

In addition, Node.js has two intermediate queues

  • Next Ticks queue

  • Other Microtasks queue

Node.js differs from the Event Loop in the browser

We can review JavaScript event loops in browsers by moving on to another of my columns in the Advanced Front End Engineer series – JavaScript event loops in browsers

After coming back, the conclusion:

In browsers, the microTask task queue is executed after each MacroTask has been executed.

In Node.js, microTasks are executed between phases of the event cycle, i.e. tasks in the MicroTask queue are executed after each phase is completed.

The Macrotask in this article is called a task in the WHATWG. Macrotask has no actual source for ease of understanding.

Compared to the browser, Node offers setImmediate and Process.Nexttick asynchronous actions.

SetImmediate’s callback function is executed in the Check phase. Process. nextTick is treated as a microtask, and all microtasks are executed at the end of each phase. You can think of process.nextTick as jumping the queue and executing before the next phase.

Process. nextTick The harm caused by queue-jumping

The process.nextTick callback causes the event loop to fail to proceed to the next phase. The I/O cannot be executed after the I/O processing is complete or the timer expires. To prevent this from starving other event handlers, Node.js provides a process.maxTickDepth(1000 by default).

Microtasks in Node.js

  • process.nextTick()
  • Promise.then()
Promise.resolve().then(function(){
    console.log('then')
})
process.nextTick(function(){
    console.log('nextTick')});// nextTick
// then
Copy the code

We can see that nextTick is executed earlier than THEN.

Node.js v11 change event loop

Since Node.js V11, the principle of the event loop has changed, with microTask queues executed as soon as macroTask is executed in the same phase, consistent with browser behavior. Please refer to this PR for details.

❤️ Read three things

1. Please give me a thumbs-up when you see this. Your thumbs-up is the motivation for my creation.

2. Pay attention to the front canteen of the public number, your front canteen, remember to eat on time!

It’s winter, wear more clothes and don’t catch cold ~!