A preface

This article will explain in detail two parts of Asynchronous I/O and event loop which are difficult to understand in NodeJS, and sort out and supplement the core knowledge points of NodeJS.

I hope that if you feel good after reading the rose, you can give me a thumbs-up and encourage me to continue to create front-end hard text.

As usual, we begin today’s analysis with a question πŸ€”πŸ€”πŸ€” :

  • 1 What about asynchronous I/O for NodeJS?
  • What is nodeJS’s event loop?
  • 3 What are the stages of the nodeJS event cycle?
  • 4 What is the difference between Promise and nextTick in NodeJS?
  • 5. SetImmediate does not mediate and setTimeout do not mediate.
  • 6 is setTimeout accurate? What affects the execution of setTimeout?
  • What is the difference between an event loop in NodeJS and a browser?

Two asynchronous I/O

concept

Processor access to any external data resources such as registers and Cache can be regarded as I/O operations, including memory, disks, graphics cards and other external devices. In Nodejs, operations such as developers calling FS to read local files or network requests are I/O operations. (The most common abstract I/O are file operations and TCP/UDP network operations)

Nodejs is single-threaded. In single-threaded mode, all tasks are executed sequentially. However, if the previous task takes a long time, subsequent tasks will be affected. This blocks the execution of tasks and causes poor utilization of resources.

To solve these problems, Nodejs uses asynchronous I/O to make single threads stop blocking and use resources more efficiently.

Asynchronous I/O in Nodejs

Front-end developers may be more aware of asynchronous JS tasks in the browser environment, such as making an Ajax request. Just as Ajax is the API that the browser provides to THE JS execution environment, HTTP modules are provided in Nodejs to enable JS to do the same thing. Such as listening | to send HTTP requests, in addition to HTTP, NodeJS has fs file systems that operate on local files, etc.

The fs HTTP tasks above are called I/O tasks in NodeJS. Now that you understand I/O tasks, let’s examine the two forms of I/O tasks in Nodejs — blocking and non-blocking.

Synchronous and asynchronous IO modes in NodeJS

Nodejs provides both blocking and non-blocking usage for most I/O operations. Blocking refers to the fact that an I/O operation must wait for the result before proceeding to the JS code. Here’s the blocking code

Synchronize the I/O mode

/ *TODO:Blocking the * /
const fs = require('fs');
const data = fs.readFileSync('./file.js');
console.log(data)
Copy the code
  • Code blocking: read from the same directoryfile.jsFile, resultdata δΈΊ bufferStructure so that when reading, it blocks the execution of the code, soconsole.log(data)Will block and print normally only when the result is returneddata 。
  • Exception handling: The fatal point of the above operation is that if an exception occurs (such as no file.js file in the same directory), the entire program will report an error and the rest of the code will not execute. You usually need a try catch to catch error boundaries. The code is as follows:
/ *TODO:Block - Catch exception */
try{
    const fs = require('fs');
    const data = fs.readFileSync('./file1.js');
    console.log(data)
}catch(e){
    console.log('Error:',e)
}
console.log('Normal execution')
Copy the code
  • If an error occurs, it does not affect subsequent code execution or application exit due to an error.

Synchronous I/O mode causes code execution to wait for I/O results, wasting waiting time, underutilizing CPU processing power, and causing an entire thread to exit due to I/O failures. Blocking I/O across the call stack looks like this:

Asynchronous I/O mode

This is the asynchronous I/O we just introduced. Let’s first look at I/O operations in asynchronous mode:

/ *TODO:Non-blocking - Asynchronous I/O */
const fs = require('fs')
fs.readFile('./file.js'.(err,data) = >{
    console.log(err,data) // null 
      
})
console.log(111) // 111 is printed first

fs.readFile('./file1.js'.(err,data) = >{
    console.log(err,data) // save [no such file or directory, open './file1.js'].
})
Copy the code
  • The callback is executed asynchronously, returning an error message as the first argument, and returning if there is no errornull, the second parameter isfs.readFileThe real content of the implementation.
  • This asynchronous form can elegantly catch errors in performing I/O, such as being readfile1.jsIf the file cannot be found, the exception is passed directly to the callback as the first argument.

Callback, for example, is an asynchronous callback function that, like the FN of setTimeout(fn), does not block code execution. Will be triggered when the result is received, and the details of the ASYNCHRONOUS I/O callback to Nodejs will be dissected later.

For asynchronous I/O processing, Nodejs internally uses a thread pool to handle asynchronous I/O tasks, which can have multiple I/O threads processing asynchronous I/O operations at the same time, as in the example above, throughout the I/O model.

Let’s explore asynchronous I/O execution.

Event loop

Like browsers, Nodejs has its own execution model, eventLoop, which is influenced by the host environment and is not part of a javascript execution engine such as V8. This leads to different event loop modes and mechanisms in different host environments, which is intuitively reflected in the differences in microtask and macrotask processing between Nodejs and browser environments. The Nodejs event cycle and each of its stages will be discussed in more detail next.

Nodejs’s event loop has several phases, one of which is dedicated to handling I/O callbacks. Each execution phase is called a Tick, and each Tick checks for additional events and associated callback functions, such as asynchronous I/O callbacks. During the I/O processing phase, it checks whether the current I/O is complete. If so, the corresponding I/O callback function is executed. The observer who checks whether the I/O is complete is called the I/O observer.

The observer

As above mentioned the concept of I/O observer, also can have the multiple stages about Nodejs, in fact every stage has one or more corresponding observer, their work is clear in the process of each corresponding Tick, the corresponding observers did find corresponding event execution, if you have, then take out to perform.

Browser events are generated from user interactions and network requests such as Ajax. In Nodejs, events are generated from network requests such as HTTP and file I/O. These events have corresponding observers, and I have listed some important observers here.

  • File I/O operations — I/O observer;
  • Network I/O operations — Network I/O observer;
  • Process. nextTick — Idle observer
  • SetImmediate — Check the observer
  • SetTimeout /setInterval — Delay observer
  • .

In Nodejs, corresponding observers receive events of the corresponding type. During the event cycle, they will be asked if there is any task that should be executed. If there is, the observer will take out the task and give it to the event cycle to execute.

Request object and thread pool

The request object plays an important role from a JavaScript call to the time the computer system executes an I/O callback. Again, let’s take an asynchronous I/O operation as an example

Request object: Like the previous call to fs.readfile, which essentially creates a request object by calling a method on libuv. This request object holds information about the I/O request, including the body of the I/O and the callback function. The first phase of the asynchronous call is then complete, the JavaScript continues to execute the code logic on the execution stack, and the current I/O operation is put into the thread pool as a request object, waiting to be executed. Asynchronous I/O is implemented.

The thread pool: Nodejs’s thread pool is provided by the kernel IOCP on Windows and implemented by Libuv on Unix systems. The thread pool is used to perform partial I/O (operations on system files). By default, the thread pool size is 4. How do I/O operations in a thread pool work? As mentioned in the previous step, an asynchronous I/O puts the request object in the thread pool, determines whether the current thread pool is available, and if so, performs the I/O operation on the request object and returns the result to the request object. During the I/O processing phase of the event loop, the I/O observer retrieves the completed I/O object and then retrieves the callback function and results to invoke execution. The I/O callback is executed and the result is retrieved in the callback argument.

Asynchronous I/O operation mechanism

The above describes the entire asynchronous I/O execution process, from an asynchronous I/O trigger to I/O callback to execution. Event loops, observers, request objects, and thread pools comprise the entire asynchronous I/O execution model.

Use a picture to show the relationship of the four:

To summarize the above process:

  • The first stage: for each asynchronous I/O call, request parameters and callback are set in nodeJS layer to form the request object.

  • Phase 2: The request object is put into the thread pool. If there are idle I/O threads in the thread pool, the I/O task will be executed and the result will be obtained.

  • The third stage: THE I/O observer in the event loop will find the I/O request object that has obtained the result from the request object, take out the result and the callback function, put the callback function into the event loop, execute the callback, and complete the asynchronous I/O task.

  • How do I sense the completion of an asynchronous I/O task? And how to capture the completed tasks? Libuv, as an intermediate layer, is implemented on different platforms through ePoll polling under Unix, IOCP under Windows, and KQueue under FreeBSD.

Cycle of three events

The event loop mechanism is implemented by the host environment

As mentioned above, the event loop is not part of the JavaScript engine. The event loop mechanism is implemented by the host environment, so the event loop is different in different hosting environments, whether it is a browser environment or a NodeJS environment, but in different operating systems, Nodejs hosts different environments, so here’s a diagram that describes the relationship between the event loop in NodeJS and the javascript engine.

Taking the nodeJS event cycle under Libuv as a reference, the relationship is as follows:

Taking the javaScript event loop in the browser as a reference, the relationship is as follows:

The event loop is essentially like a while loop, as shown below. Let me simulate the execution flow of the event loop with a piece of code.

const queue = [ ... ]   // Queue contains events to be processed
while(true) {// Start the loop
    // Execute tasks in queue
    //....

    if(queue.length ===0) {return // Exit the process}}Copy the code
  • When Nodejs starts, just like creating a while loop,queueIt holds events that are waiting to be processed, and each time you go through the loop, if there are still events, you take the event, you execute the event, if there is a callback associated with the event, you execute the callback, and then you start the next loop.
  • If there are no events in the loop body, the process exits.

I summarized the flowchart as follows:

So how does the event loop handle these tasks? We list some common event tasks in Nodejs:

  • setTimeout ζˆ– setIntervalDelay timer.
  • Asynchronous I/O tasks: file tasks, network requests, etc.
  • setImmediateTask.
  • process.nextTickTask.
  • PromiseThe task.

We’ll talk about how these tasks work and how NodeJS handles them.

1 Event cycle phase

For different event tasks, they are executed in different event cycle phases. According to the NodeJS documentation, in general, the nodeJS event loop may have special phases depending on the operating system, but generally it can be divided into the following six phases (the six phases of the code block) :

/ * β”Œ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ┐ β”Œ ─ > β”‚ timers β”‚ - > timer, The execution of the decelerator β”‚ β”” ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ┬ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ β”˜ β”‚ β”Œ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ β”΄ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ┐ β”‚ β”‚ pending callbacks β”‚ β”‚ - > I/o β”” ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ┬ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ β”˜ β”‚ β”Œ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ β”΄ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ┐ β”‚ β”‚ idle, Prepare β”‚ β”‚ β”” ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ┬ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ β”˜ β”Œ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ┐ β”‚ β”Œ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ β”΄ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ┐ β”‚ incoming: β”‚ β”‚ β”‚ poll β”‚ < ─ ─ ─ ─ ─ ─ connections, β”‚ β”‚ β”” ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ┬ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ β”˜ β”‚ data, Etc. β”‚ β”‚ β”Œ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ β”΄ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ┐ β”” ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ β”˜ β”‚ β”‚ check β”‚ β”‚ β”” ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ┬ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ β”˜ β”‚ β”Œ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ β”΄ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ┐ β”” ─ ─ ─ close callbacks β”‚ β”” ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ β”˜ * /
Copy the code
  • The first phase: timer, the main thing that the timer phase does is to execute the callback function registered by setTimeout or setInterval.

  • The second stage is called pending callback. Most OF the I/O callback tasks are executed in the POLL stage, but there are also some delayed I/O callback functions left over from the previous event loop. Therefore, this stage is used to call the I/O callback functions delayed by the previous event loop.

  • Phase 3: Idle Prepare, which is used only for the internal NodeJS module.

  • The fourth stage: poll polling stage. This stage mainly does two things. One stage performs callback function of asynchronous I/O. Calculate the time when the current polling phase blocks subsequent phases.

  • The fifth phase: The Check phase, which starts when the poll callback queue is empty, executes the setImmediate callback.

  • Stage 6: The close stage performs the callback function that registers the close event.

For the execution characteristics and corresponding event tasks of each stage, I will analyze them in detail next. Let’s take a look at how the six phases are represented in the underlying source code.

Take a look at the source code for the NodeJS event loop under Libuv. :

libuv/src/unix/core.c

int uv_run(uv_loop_t* loop, uv_run_mode mode) {
  // Omit the previous process.
  while(r ! =0 && loop->stop_flag == 0) {

    /* Update the event loop */ 
    uv__update_time(loop);

    /* The first phase: the timer phase executes */
    uv__run_timers(loop);

    /* Second stage: Pending stage */
    ran_pending = uv__run_pending(loop);

    /* Idle prepare */
    uv__run_idle(loop);
    uv__run_prepare(loop);

    timeout = 0;
    if((mode == UV_RUN_ONCE && ! ran_pending) || mode == UV_RUN_DEFAULT)/* Computes the timeout time */
      timeout = uv_backend_timeout(loop);
    
    /* Stage 4: poll stage */
    uv__io_poll(loop, timeout);

    /* Check phase */
    uv__run_check(loop);
    /* Phase 6: close phase */
    uv__run_closing_handles(loop);
    /* Check that the current thread has tasks */ 
     r = uv__loop_alive(loop);

    /* omit the following process */
  }
  return r;
}
Copy the code
  • We see that the six phases are executed sequentially, and only when the tasks of the previous phase are completed can the next phase proceed
  • whenuv__loop_aliveDetermine that the current event loop has no task and exit the thread.

2 Task Queue

Throughout the event loop, four queues (the actual data structure is not a queue) are executed in libuv’s event loop, and two queues are executed in NodeJS: the Promise queue and the nextTick queue.

There is more than one queue in NodeJS, and different types of events are enqueued in their own queues. After processing one phase, the event loop processes the two intermediate queues until they are empty before moving to the next phase.

Libuv handles the task queue

Each phase of the event loop executes the contents of the corresponding task queue.

  • Timer queue (PriorityQueue) : Essentially a data structure that is a binary minimum heap. The root node of the binary minimum heap retrieves the callback function corresponding to the timer on the nearest timeline.

  • I/O event queue: stores I/O tasks.

  • ImmediateList: Multiple Immediate, node layers are stored using a linked list data structure.

  • Close the callback event queue: Place the callback function to close.

Non-libuv intermediate queue

  • NextTick queue: The callback function that holds nextTick. This is unique to NodeJS.
  • Microtasks microqueue Promise: callback function that stores promises.

Intermediate queue execution characteristics:

  • The first step is to understand that the two intermediate queues are not executed in Libuv, they are executed in nodeJS layer, after the libuv layer processes each phase of the task, the node layer will communicate, so the tasks in the two queues will be processed first.

  • The nextTick task has a higher priority than the Promise callback in the Microtasks task. That is, Node clears the tasks in nextTick first, and then the Promise tasks. To test this conclusion, here is an example of a printed result:

/ *TODO:Print order */
setTimeout(() = >{
    console.log('setTimeout execution')},0)

const p = new Promise((resolve) = >{
     console.log('Promise to perform)
     resolve()
})
p.then(() = >{
    console.log('Promise callback execution ')
})

process.nextTick(() = >{
    console.log('nextTick execution')})console.log('Code execution complete')
Copy the code

What is the order of execution in NodeJS in the code block above?

Effect:

Print result: Promise execution -> code execution completed -> nextTick execution -> Promise callback execution -> setTimeout execution

Explanation: It’s easy to see why. In the main code event loop, Promise execution and code completion are printed first, nextTick is put into the nextTick queue, Promise callback is put into the Microtasks queue, SetTimeout is put into the timer heap. Next, the nextTick queue is emptied, and the nextTick execution is printed. Then, the Microtasks queue is emptied, and the Promise callback execution is printed. Finally, the timer task is determined in the event loop loop. So start a new event loop, first the timer task is executed, setTimeout execution is printed. The whole process is complete.

  • The code in both nextTick and Promise tasks blocks an orderly event loop, causing I/O starvation, so the logic in both tasks needs to be handled carefully. For example:
/ *TODO:Blocked I/O */
process.nextTick(() = >{
    const now = +new Date(a)/* Block code for three seconds */
    while(+new Date() < now + 3000 ){}
})

fs.readFile('./file.js'.() = >{
    console.log('I/O: file ')})setTimeout(() = > {
    console.log('setTimeout: ')},0);
Copy the code

Effect:

  • It takes three seconds for timer tasks and I/O tasks in the event loop to be executed in order. That is to say,nextTickBlock the orderly progression of the event loop.

3 Event cycle flowchart

Next, a flowchart is used to represent the execution sequence of the six phases of the event cycle, as well as the execution logic of the two priority queues.

4 Timer Phase -> Timer Timer/delay interval

Expired timers and Intervals: The timer observer checks for asynchronous tasks created by setTimeout or setInterval. The internal principle is similar to asynchronous I/O, but the timer/timer implementation does not use thread pools. The timer object passed by setTimeout or setInterval will be inserted into the binary minimum heap inside the timer observer of the delay timer. During each event cycle, the timer object will be removed from the top of the binary minimum heap to determine whether the timer/interval has expired. If so, it will be called to queue up. The first item in the current queue is checked again until no one has expired and moved to the next stage.

How does the Libuv layer handle timers

First let’s take a look at how the Libuv layer handles the timer

libuv/src/timer.c

void uv__run_timers(uv_loop_t* loop) {
  struct heap_node* heap_node;
  uv_timer_t* handle;

  for (;;) {
    /* Find the root node (minimum value) in timer_heap in loop */  
    heap_node = heap_min((struct heap*) &loop->timer_heap);
    / * * /
    if (heap_node == NULL)
      break;

    handle = container_of(heap_node, uv_timer_t, heap_node);
    if (handle->timeout > loop->time)
      /* If the execution time is longer than the event loop event, there is no need to execute */ in this loop
      break;

    uv_timer_stop(handle);
    uv_timer_again(handle);
    handle->timer_cb(handle); }}Copy the code
  • The above handle timeout can be interpreted as the expiration time, which is the time when the timer returns to the function.
  • When timeout is greater than the start time of the current event loop, the callback should not be executed. Therefore, according to the nature of the binary minimum heap, the parent node is always smaller than the child node, so if the time nodes of the root node do not meet the execution time, the other timers also do not meet the execution time. At this point, the callback function of the timer phase is exited and the next phase of the event cycle is directly entered.
  • When the expiration time is less than the start time of the current event loop tick, indicating that at least one timer has expired, the loop iterates over the root node of the timer minimum heap and invokes the corresponding callback function of the timer. Each iteration of the loop updates the timer at the root of the smallest heap to the nearest time node.

The above is the execution characteristic of the Timer phase in Libuv. Next, look at how node handles timer delayers.

How does the Node layer handle timers

Nodejs implements setTimeout and setInterval.

node/lib/timers.js

function setTimeout(After the callback,){
    / /...
    /* Check the argument logic */
    / /..
    /* Create a timer observer */
    const timeout = new Timeout(callback, after, args, false.true);
    /* Inserts the timer observer into the timer heap */
    insert(timeout, timeout._idleTimeout);

    return timeout;
}
Copy the code
  • SetTimeout: The logic is simple: create a timer observer and place it in the timer heap.

So what does Timeout do?

node/lib/internal/timers.js

function Timeout(callback, after, args, isRepeat, isRefed) {
  after *= 1 
  if(! (after >=1 && after <= 2ζ°‘θΏεˆ†ε­31 - 1)) {
    after = 1 // If timeout is 0 or greater than 2 ** 31-1, set it to 1
  }
  this._idleTimeout = after; // Delay time
  this._idlePrev = this;
  this._idleNext = this;
  this._idleStart = null;
  this._onTimeout = null;
  this._onTimeout = callback; // The callback function
  this._timerArgs = args;
  this._repeat = isRepeat ? after : null;
  this._destroyed = false;  

  initAsyncResource(this.'Timeout');
}
Copy the code
  • Both setTimeout and setInterval are essentially Timeout classes in NodeJS. The maximum time valve is exceeded2 times * 31 minus 1orsetTimeout(callback, 0), _idleTimeout is set to 1, which is converted to setTimeout(callback, 1) for execution.

Timer processing flow chart

To illustrate the flow diagram, we create a timer, and then the timer executes in the event loop.

The timer feature

Two things to note here:

  • Execution mechanism: The delayer timer observer, which executes one at a time, empts nextTick and Promise after executing one, expiration time is an important factor in determining whether either of them executes, and a little bit of poll counts how long the block timer executes, It also has an important influence on the execution of tasks in the timer stage.

To verify the conclusion, execute the timer task one at a time. Let’s start with a code snippet:

setTimeout(() = >{
    console.log('setTimeout1:')
    process.nextTick(() = >{
        console.log('nextTick')})},0)
setTimeout(() = >{
    console.log('setTimeout2:')},0)
Copy the code

Print result:

The nextTick queue is executed at the end of each phase of the event cycle. Both delayers have a threshold of 0. If the expired task is executed at one time during the timer phase, then setTimeout1 -> setTimeout2 -> nextTick is printed. You actually execute a timer task, then the nextTick task, and then the next timer task.

  • Accuracy problem: With regard to setTimeout counters, timers are not exact, although the event loop on NodeJS is very fast, but from the creation of the timeout class, which takes up some events, to context execution, I/O execution, nextTick queue execution, The execution of Microtasks blocks the execution of the delayer. Even checking for timer expiration can consume some CPU time.

  • Performance issues: If you want to use setTimeout(fn,0) to execute some tasks that are not called immediately, the performance is not as good as that of process.nextTick. Firstly, setTimeout is not precise enough. It takes up a bit of performance, so you can solve this scenario with process.nexttick.

5 pending phase

The Pending stage is used to handle the DELAYED I/O callbacks that precede this event loop. First, take a look at execution timing in Libuv.

libuv/src/unix/core.c

static int uv__run_pending(uv_loop_t* loop) {
  QUEUE* q;
  QUEUE pq;
  uv__io_t* w
  /* Pending_queue is empty, empty the queue, return 0 */
  if (QUEUE_EMPTY(&loop->pending_queue))
    return 0;
  
  QUEUE_MOVE(&loop->pending_queue, &pq);
  while (!QUEUE_EMPTY(&pq)) { Pending_queue empties I/O callback if pending_queue is not empty. Return 1 * /
    q = QUEUE_HEAD(&pq);
    QUEUE_REMOVE(q);
    QUEUE_INIT(q);
    w = QUEUE_DATA(q, uv__io_t, pending_queue);
    w->cb(loop, w, POLLOUT);
  }
  return 1;
}
Copy the code
  • If you store the I/O callback taskpending_queueIs empty, so return 0.
  • If pending_queue has an I/O callback task, execute the callback task.

6 Idle, prepare

Idle does some libuv internal operations and prepares for the following I/O polling. Let’s take a look at the more important poll phase.

7 Poll INDICATES the I/O polling phase

Before explaining what the poll phase does, let’s first look at the polling phase execution logic in Libuv:

  timeout = 0;
    if((mode == UV_RUN_ONCE && ! ran_pending) || mode == UV_RUN_DEFAULT)/* Computes timeout */
      timeout = uv_backend_timeout(loop);
      /* Enter I/O polling */
      uv__io_poll(loop, timeout);
Copy the code
  • Initialization timeout period timeout = 0, passuv_backend_timeoutCalculate thepollPhase timeout. The timeout affects the execution of asynchronous I/O and subsequent event loops.

What does timeout stand for

First, understand what different timeouts stand for in I/O polling.

  • whentimeout = 0, indicating that the poll phase does not block the progress of the event cycle, indicating that there is a task that is more urgent to execute. Then the current poll stage will not block and will enter the next stage as soon as possible. The current tick will end as soon as possible and enter the next event cycleemergencyThe mission will be executed.
  • whentimeout = -1, the announcement will block the event loop for a long time, so you can stay in the POLL phase of asynchronous I/O and wait for the new I/O task to complete.
  • whentimeoutIs equal to a constant, indicating the time that the IO poll cycle stage can stay at this time. Then when timeout will be a constant will be revealed soon.

Get a timeout

Timeout is obtained by uv_backend_timeout so how do you get timeout?

int uv_backend_timeout(const uv_loop_t* loop) {
    /* The current event loop task stops without blocking */
  if(loop->stop_flag ! =0)
    return 0;
   /* Does not block when the current event loop is inactive */
  if(! uv__has_active_handles(loop) && ! uv__has_active_reqs(loop))return 0;
  /* If idle handle queue is not empty, 0 is returned, that is, not blocked. * /
  if(! QUEUE_EMPTY(&loop->idle_handles))return 0;
   /* I/O pending when the queue is not empty. * /  
  if(! QUEUE_EMPTY(&loop->pending_queue))return 0;
   /* There is a close callback */
  if (loop->closing_handles)
    return 0;
  /* Calculate whether there is a minimum delay delay | timer */
  return uv__next_timeout(loop);
}
Copy the code

Uv_backend_timeout mainly does:

  • Does not block when the current event loop stops.
  • Does not block while the current event loop is inactive.
  • 0 is returned when idle mediate (setImmediate) is not empty.
  • An I/O pending queue is not blocked when it is not empty.
  • Does not block when the callback function is closed.
  • If none of the above is met, passuv__next_timeoutCalculate whether there is a minimum delay threshold timer | delay timer (the most urgent execution), return the delay time.

Next look at the uv__next_timeout logic.

int uv__next_timeout(const uv_loop_t* loop) {
  const struct heap_node* heap_node;
  const uv_timer_t* handle;
  uint64_t diff;
  /* Find the smallest delay timer */
  heap_node = heap_min((const struct heap*) &loop->timer_heap);
  if (heap_node == NULL) /* If there is no timer, return -1, poll state */
    return - 1; 

  handle = container_of(heap_node, uv_timer_t, heap_node);
   /* There are expired timer tasks, so return 0, poll phase does not block */
  if (handle->timeout <= loop->time)
    return 0;
  /* Return the current minimum threshold timer subtracted from the current event loop events, the resulting time, which proves how long the poll can linger */ 
  diff = handle->timeout - loop->time;
  return (int) diff;
}
Copy the code

Uv__next_timeout does the following:

  • Find the timer with the smallest time threshold. If there is no timer, return -1. The poll phase will block indefinitely. The advantage of this is that once an I/O completes, the I/O callback function is added directly to the poll and the corresponding callback function is then executed.
  • If you have a timer, buttimeout <= loop.timeThe poll phase is not blocked, and the expired task is executed first.
  • If there is no expiration, the timer that returns the current minimum threshold is subtracted from the events of the current event loop, which proves how long the poll can linger. When the stay is over, it is proved that there is an expiration timer, and then enter the next tick.

Perform io_poll

This is followed by the actual execution of uv__io_poll, which has an epoll_wait method that polls for I/O completion according to timeout, and then performs an I/O callback if so. This is also an important part of asynchronous I/O implementation under Unix.

Poll phase nature

To summarize the nature of the poll phase:

  • The poll phase is a timeout to determine whether to block the event loop. Poll is also polling for I/O tasks, and the event cycle tends to continue in the POLL phase, which is intended to execute I/O tasks faster. If there are no other tasks, then the poll phase will always be there.
  • If there are tasks for other phases that are more urgent to execute, such as timer, close, the poll phase will not block and the next tick phase will proceed.

Poll phase flow chart

What I did in the whole poll phase was shown as a flow chart, leaving out some details.

8 the check phase

If the poll phase enters the Idle state and the setImmediate callback exists, the poll phase breaks the unrestricted wait state and enters the Check phase to perform the check phase’s callback.

All Check does is handle setImmediate callbacks. So let’s look at setImmediate in Nodejs.

SetImmediate in the underlying Nodejs

SetImmediate definition

node/lib/timer.js

function setImmediate(callback, arg1, arg2, arg3) {
  validateCallback(callback); /* Check the callback function */
   /* Create an Immediate class */
   return new Immediate(callback, args);
}
Copy the code
  • When callingsetImmediateEssentially calls the setImmediate method in NodeJS, first verifies the callback function, and then creates oneImmediateClass. Next, look at the Immediate class.

node/lib/internal/timers.js

class Immediate{
   constructor(callback, args) {
    this._idleNext = null;
    this._idlePrev = null; /* Initialization parameters */
    this._onImmediate = callback;
    this._argv = args;
    this._destroyed = false;
    this[kRefed] = false;

    initAsyncResource(this.'Immediate');
    this.ref();
    immediateInfo[kCount]++;
    
    immediateQueue.append(this); Add * / / *}}Copy the code
  • The Immediate class initializes some parameters and then inserts the current Immediate class intoimmediateQueueChain in the table.
  • The immediateQueue is essentially a linked list containing each Immediate.

SetImmediate perform

Immediately after the poll phase, the Check phase is immediateQueue, and the Immediate in its immediateQueue is executed. In each event loop, a setImmediate callback is executed and the nextTick and Promise queues are emptied. To verify this, as with setTimeout, look at the following code block:

setImmediate(() = >{
    console.log('setImmediate1')
    process.nextTick(() = >{
        console.log('nextTick')
    })
})

setImmediate(() = >{
    console.log('setImmediate2')})Copy the code

Print setImmediate1 -> nextTick -> setImmediate2. In each event loop, perform a setImmediate, and then perform clearing the nextTick queue. In the next event loop, Execute another setImmediate2.

SetImmediate Perform flow chart

setTimeout & setImmediate

SetTimeout (FN,0) and setImmediate(FN) compare setTimeout(FN,0) to setImmediate(FN) if the developer expects to perform asynchronous tasks with delay.

  • SetTimeout is used to execute the callback function within the minimum error of setting the threshold value. SetTimeout has precision problems, and the creation of setTimeout and poll stage may affect the execution of setTimeout callback function.
  • SetImmediate Immediately after the poll phase, setImmediate goes to the Check phase and executessetImmediateThe callback.

If setTimeout and setImmediate go together, then who executes first?

First write a demo:

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

setImmediate(() = >{
    console.log( 'setImmediate')})Copy the code

guess

SetTimeout does occur in the timer phase, and setImmediate does occur in the Check phase before the Timer phase, so setTimeout takes precedence over setImmediate. But is that the case?

Actual print result

SetTimeout (fn,1) and setImmediate mediate mediate mediate (FN,1) do not mediate (fn,1). SetTimeout (fn,1) does not mediate. When the synchronization code of the main process is executed, it will enter the event cycle stage and enter the timer for the first time. At this time, the time threshold of the timer corresponding to settimeout is 1. If in the previous uv__run_timer(loop), The setImmediate callback is not used in the Timer phase, and the timer does not expire. The setImmediate callback is not used in the Timer phase. The setImmediate callback is not used in the Timer phase. SetImmediate – > setTimeout.

However, if the timer phase is longer than a millisecond, the sequence changes. The Timer phase is used to retrieve the expired setTimeout task, and then the Check phase is used to perform setImmediate. SetTimeout -> setImmediate

The reason for this is that the interval between the time check of the timer and the tick of the current event cycle may be less than 1ms or greater than the threshold of 1ms, which determines whether setTimeout will be executed in the first event cycle.

The next time I use code to block, there is a high probability that setTimeout will always be executed before setImmediate.

/ *TODO:  setTimeout & setImmediate */
setImmediate(() = >{
    console.log( 'setImmediate')})setTimeout(() = >{
    console.log('setTimeout')},0)
/* Block the code with 100000 loops to make setTimeout expire */
for(let i=0; i<100000; i++){ }Copy the code

Effect:

100000 loops block the code so that setTimeout will exceed the time threshold, thus ensuring that setTimeout -> setImmediate is executed first each time.

Special case: Determine sequential consistency. Let’s look at the special case.

const fs = require('fs')
fs.readFile('./file.js'.() = >{
    setImmediate(() = >{
        console.log( 'setImmediate')})setTimeout(() = >{
        console.log('setTimeout')},0)})Copy the code

SetImmediate takes precedence over setTimeout, so let’s look at why.

  • First, analyze asynchronous tasks — there is an asynchronous I/O task in the main process, and a setImmediate and a setTimeout in the I/O callback.
  • inpollPhase performs I/O callbacks. Then deal with a setImmediate

As long as you master the characteristics of the above stages, you can clearly distinguish the execution of different situations.

9 the close phase

The close phase is used to execute some closed callback functions. Execute all close events. Next look at the implementation of the close event libuv.

libuv/src/unix/core.c

static void uv__run_closing_handles(uv_loop_t* loop) {
  uv_handle_t* p;
  uv_handle_t* q;

  p = loop->closing_handles;
  loop->closing_handles = NULL;

  while (p) {
    q = p->next_closing;
    uv__finish_close(p); p = q; }}Copy the code
  • uv__run_closing_handlesThis method loops through the callback function in the close queue.

10 Nodejs event loop summary

Next, summarize the Nodejs event loop.

  • Nodejs’s event cycle is divided into six phases. They are timer stage, Pending stage, prepare stage, poll stage, Check stage, and close stage respectively.

  • NextTick queue and Microtasks queue are executed after each phase is completed. NextTick has a higher priority than Microtasks (Promise).

  • The poll phase mainly deals with I/O and, if there are no other tasks, is in the polling blocking phase.

  • The Timer phase deals primarily with timers/delayers, which are not accurate and require additional performance waste to create, and their execution is affected by the Poll phase.

  • The Pending phase handles callback tasks for I/O expiration.

  • The Check phase handles setImmediate. SetImmediate and setTimeout perform timing and distinction.

Four Nodejs event loop exercise

Next, in order to understand the flow of the event loop, here are two event loop problems. As a practice:

Problem sets a

process.nextTick(function(){
    console.log('1');
});
process.nextTick(function(){
    console.log('2');
     setImmediate(function(){
        console.log('3');
    });
    process.nextTick(function(){
        console.log('4');
    });
});

setImmediate(function(){
    console.log('5');
     process.nextTick(function(){
        console.log('6');
    });
    setImmediate(function(){
        console.log('7');
    });
});

setTimeout(e= >{
    console.log(8);
    new Promise((resolve,reject) = >{
        console.log(8+'promise');
        resolve();
    }).then(e= >{
        console.log(8+'promise+then'); })},0)

setTimeout(e= >{ console.log(9); },0)

setImmediate(function(){
    console.log('10');
    process.nextTick(function(){
        console.log('11');
    });
    process.nextTick(function(){
        console.log('12');
    });
    setImmediate(function(){
        console.log('13');
    });
});

console.log('14');
 new Promise((resolve,reject) = >{
    console.log(15);
    resolve();
}).then(e= >{
    console.log(16);
})
Copy the code

It might be confusing if you just saw the demo, but it’s easy to look at the whole event loop, so let’s analyze the overall flow:

  • Phase 1: Start js file first, then enter the first event loop, then execute the synchronization task first:

Print first:

Print the console. The log (‘ 14 ‘);

Print the console. The log (15);

NextTick queue:

nextTick -> console.log(1) nextTick -> console.log(2) -> setImmediate(3) -> nextTick(4)

Promise the queue

Promise.then(16)

Check the queue

setImmediate(5) -> nextTick(6) -> setImmediate(7) setImmediate(10) -> nextTick(11) -> nextTick(12) -> setImmediate(13)

The timer queue

setTimeout(8) -> promise(8+’promise’) -> promise.then(8+’promise+then’) setTimeout(9)

  • Phase 2: Before entering the new event loop, clear the nextTick queue and the Promise queue in the order that the nextTick queue is larger than the Promise queue.

Empty nextTick and print:

console.log(‘1’);

console.log(‘2’);

When the second nextTick is executed, another nextTick is added to the queue. Execute immediately.

console.log(‘4’)

Next, clear the Microtasks

console.log(16);

At this point, the Check queue adds the new setImmediate.

Check the Queue setImmediate(5) -> nextTick(6) -> setImmediate(10) -> nextTick(11) -> nextTick(12) -> setImmediate setImmediate(13) setImmediate(3)

  • It then enters a new event loop and executes the tasks in the timer first. Execute the first setTimeout.

Execute the first timer:

console.log(8);

A Promise is found. In a normal execution context:

console.log(8+’promise’);

Then add promise.then to the nextTick queue. I’m going to clear the nextTick queue immediately.

console.log(8+’promise+then’);

Execute the second timer:

console.log(9)

  • Next to the check phase, execute the contents of the check queue:

Perform the first check:

console.log(5);

Now we find a nextTick, and then we have a setImmediate and we add setImmediate to the check queue. NextTick is then executed.

console.log(6)

Perform the second check

console.log(10)

Two Nextticks and one setImmediate are found. Next, empty the nextTick queue. Add setImmediate to the queue.

console.log(11)

console.log(12)

The check queue looks like this:

setImmediate(3) setImmediate(7) setImmediate(13)

Next, empty the check queue in sequence. print

console.log(3)

console.log(7)

console.log(13)

At this point, execute the entire event loop. Then the overall print content is as follows:

Five summarizes

The main contents of this paper are as follows:

  • This section describes asynchronous I/O and its internal mechanism.
  • Nodejs event cycle, six stages.
  • The principle and difference of setTimeout, setImmediate, asynchronous I/O, nextTick, Promise in Nodejs.
  • Nodejs event loop practices.

The resources

  • Nodejs event loop from Libuv
  • Nodejs
  • The workflow & lifecycle of the Node.js event loop

React Advanced Practice Guide

This volume will continue to be updated as the React version is updated, with new chapters

As an early warning, the React Context principles section will be added in chapter 8.

I’d like to offer you a 30% discount code F3Z1VXtv on a first come, first served basis