This article is published on my blog

First, JS event loops for all platforms, both browsers and NodeJS, are not defined by the ECMA 262 specification. Event loops are not part of the ECMA 262 specification. Browser-side event loops are defined in Web apis and maintained by the W3C and HTML Living Standard. Nodejs is based on the Libuv event loop, there is no event loop specification standard, so the best way to understand the NodeJS event loop is the source code and official documentation of nodeJS and libuv source code and official documentation.

References in this article include official documentation, the Nodejs/Libuv repository, nodejs/ Libuv contributor answers, Google/Microsoft engineers, stackOverflow answers, etc.


Overview of event cycles

According to the Official NodeJS documentation, the nodeJS event loop usually has special phases depending on the operating system, but generally can be divided into the following six phases:

┌ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ┐ ┌ ─ > │ timers │ │ └ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ┬ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ┘ │ ┌ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ┴ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ┐ │ │ pending Callbacks │ │ └ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ┬ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ┘ │ ┌ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ┴ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ┐ │ │ idle, Prepare │ │ └ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ┬ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ┘ ┌ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ┐ │ ┌ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ┴ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ┐ │ incoming: │ │ │ poll │ < ─ ─ ─ ─ ─ ┤ connections, │ │ └ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ┬ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ┘ │ data, Etc. │ │ ┌ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ┴ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ┐ └ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ┘ │ │ check │ │ └ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ┬ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ┘ │ ┌ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ┴ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ┐ └ ─ ─ ┤ close callbacks │ └ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ┘Copy the code
  1. The timer phase is used to execute all callbacks registered with timer functions (setTimeout and setInterval).

  2. Pending Callbacks phase. Although most I/O callbacks are executed immediately in the poll phase, there are some I/O callback functions that are invoked latently. This stage is then used to invoke the I/O callback function that the previous event loop delayed.

    From the I/O loop-step-4 design document in Libuv.

  3. In the Idle prepare phase, it is used only for nodeJS internal modules.

  4. Poll phase, which has two main responsibilities: 1. Calculate the time that the current poll needs to block subsequent phases; 2. Handle event callback functions.

    Nodejs has a tendency to stay in this phase of the event loop, which will be explained later.

  5. The Check phase, used to schedule the execution of a particular code fragment using setImmediate when the callback function queue in the Poll phase is empty.

  6. The close callback phase executes all callbacks that register close events.

Each NodeJS event loop tick always goes through the above phases, starting with the Timer phase and ending with the close callback phase. Each phase loops through the current phase’s callback queue until the queue is empty or the maximum number of callbacks that can be executed is reached.

Event loop implementation

According to nodeJS documentation, the event loop in NodeJS relies on a C library called Libuv. The execution of libuv essentially determines the execution of the event loop in NodeJS.

As of this publication, the latest version of Libuv is V1.35.0.

Q: What is libuv?

A: Libuv is A single-threaded, non-blocking asynchronous I/O solution implemented in C language. In essence, it encapsulates the underlying asynchronous I/O operations of common operating systems and exposes A consistent API. The primary purpose is to provide A unified event loop model for NodeJS on different system platforms as much as possible.

The core of the nodeJS event loop corresponds to the uv_run function in Libuv. The core logic is as follows:

// http://docs.libuv.org/en/v1.x/loop.html#c.uv_loop_alive
r = uv__loop_alive(loop);
if(! r) uv__update_time(loop);// http://docs.libuv.org/en/v1.x/guide/eventloops.html#stopping-an-event-loop
while(r ! =0 && loop->stop_flag == 0) {
  // http://docs.libuv.org/en/v1.x/loop.html#c.uv_update_time
  uv__update_time(loop);
  / / the timer period
  uv__run_timers(loop);
  // Pending Callbacks
  ran_pending = uv__run_pending(loop);
  uv__run_idle(loop);
  uv__run_prepare(loop);

  timeout = 0;
  if((mode == UV_RUN_ONCE && ! ran_pending) || mode == UV_RUN_DEFAULT) timeout = uv_backend_timeout(loop);/ / poll phase
  uv__io_poll(loop, timeout);
  / / check phase
  uv__run_check(loop);
  // Close callbacks phase
  uv__run_closing_handles(loop);

  if (mode == UV_RUN_ONCE) {
    /* UV_RUN_ONCE implies forward progress: at least one callback must have * been invoked when it returns. uv__io_poll() can return without doing * I/O (meaning: no callbacks) when its timeout expires - which means we * have pending timers that satisfy the forward progress constraint. * * UV_RUN_NOWAIT makes no guarantees about progress so it's omitted from * the check. */
    uv__update_time(loop);
    uv__run_timers(loop);
  }

  r = uv__loop_alive(loop);
  if (mode == UV_RUN_ONCE || mode == UV_RUN_NOWAIT)
    break;
}
Copy the code

According to the description of IO loop in the Libuv documentation, in principle, there is no more than one event loop in a thread, and multiple parallel event loops can exist in multiple threads. The event loop follows the normal single-threaded asynchronous I/O scheme. All I/O is performed on non-blocking sockets. These sockets use the best polling mechanism for the platform given in the table below.

mechanism platform
epoll Linux
kqueue OSX, BSD
IOCP Windows
event ports SunOS

As part of the entire event loop iteration Loop Iteration, a single event loop loop blocks waiting for IO activity on sockets that have been added to the poller, and triggers the corresponding callback function indicating the socket’s condition (readable, writable, Suspend) so that the handle can read, write, or perform the desired IO operation.

r = uv__loop_alive(loop);
if(! r) uv__update_time(loop);Copy the code

According to the source code and libuv official documentation, the event loop first caches the start time of the current event loop tick to reduce time-dependent system calls.

The cache time is used because the time calls in the system are affected by other applications in the system, so the cache time is used when the tick of the event loop starts in order to avoid the influence of other applications on NodeJS.

If the event loop is active, start the current event loop, otherwise immediately exit the entire event loop iteration. So how do we define an event loop iteration as active? An event loop is said to be active if it has an active handle or reference handle, an active request or closing handle.

// http://docs.libuv.org/en/v1.x/guide/eventloops.html#stopping-an-event-loop
while(r ! =0 && loop->stop_flag == 0) {
  // ...
}
Copy the code

As you can see from the above sample code, the entire event loop iteration is a while infinite loop, and it is the while statement that continuously pushes the iteration of the event loop. At the beginning of each iteration of the loop, the current event loop tick is continuously verified to be active without a stop flag. After entering the loop, it first updates the start time of the current event loop and continues to execute the queue of callback functions for each phase of the event loop.

Combined with the above abstract summary of the life cycle of nodeJS event cycle, it is not difficult to conclude according to the core logic of uv_run()doc:

  1. Timer phase: uv__run_timers(loop)

  2. Pending Callbacks: uv__run_pending(loop)

  3. Idle phase: uv__run_idle(loop)

  4. Poll phase: uv__io_poll(loop, timeout)

  5. Uv__run_check (loop)

  6. Close Callbacks: uv__run_closing_handles(loop) function definition

The timer period

A tick in the NodeJS event loop always starts with a timer phase and contains a queue of callback functions to execute registered by all setTimeouts and setIntervals. The core responsibility of this phase is to execute callback functions registered by all timers that have reached the time threshold.

To be executed: a timer registered callback that is added to the timer callback queue and is waiting to be executed when the timer’s time threshold has been reached.

It’s worth noting that none of the timer implementations, whether in NodeJS or a Web browser, guarantee that the callback will be executed immediately after the time threshold is reached, only that the callback registered by the timer will be executed as soon as the time threshold is reached.

const NS_PER_SEC = 1e9
const time = process.hrtime()
// [1800216, 25]

setTimeout((a)= > {
  const diff = process.hrtime(time)
  // [1, 552]

  console.log(`Benchmark took ${diff[0] * NS_PER_SEC + diff[1]} nanoseconds`)
  // Benchmark took 1000000552 nanoseconds
}, 1000)
Copy the code

Also, technically, the poll phase determines the timing of the timer callback function. For details, see the following explanation on the influence of poll on timer.

How does Libuv schedule timers

As mentioned above, the TIMER stage corresponds to the C function uv__run_timers(loop) in Libuv. . And the corresponding core call logic in the uv_run function body is as follows:

int timeout;
int r;
int ran_pending;

r = uv__loop_alive(loop);
if(! r) uv__update_time(loop);// http://docs.libuv.org/en/v1.x/guide/eventloops.html#stopping-an-event-loop
while(r ! =0 && loop->stop_flag == 0) {
  uv__update_time(loop);
  uv__run_timers(loop);
  // ...
}
Copy the code

Uv__update_time (loop) is always called first when a tick in the event loop is initiated; To update the start time of the current event loop tick.

UV_UNUSED(static void uv__update_time(uv_loop_t* loop)) {
  /* Use a fast time source if available. We only need millisecond precision. */
  loop->time = uv__hrtime(UV_CLOCK_FAST) / 1000000;
}
Copy the code

Here the uv__hrtime function contains the exposed time-dependent system calls of the current operating system. Time calls to the system here may be affected by other applications. Once the time of the loop structure is updated, the queue of callback functions for the timer phase is then executed. As follows:

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

  for (;;) {
    heap_node = heap_min(timer_heap(loop));
    if (heap_node == NULL)
      break;

    // container_of is used by preprocesser to implement pre-compile text substitution
    / / https://github.com/libuv/libuv/blob/v1.35.0/src/uv-common.h#L57-L58
    handle = container_of(heap_node, uv_timer_t, heap_node);

    if (handle->timeout > loop->time)
      break;

    // http://docs.libuv.org/en/v1.x/timer.html#c.uv_timer_stop
    uv_timer_stop(handle);
    // http://docs.libuv.org/en/v1.x/timer.html#c.uv_timer_againuv_timer_again(handle); handle->timer_cb(handle); }}Copy the code

It is worth noting here that all timers are stored in libuv as a binary minimum heap structure consisting of the execution time nodes of the timer callback function (i.e., time + timeout, not the timer time threshold). The root node of the binary minimum heap is used to obtain the handle of the callback function corresponding to the nearest timer on the time line, and then the execution time node of the nearest timer is obtained by the timeout value corresponding to the handle:

  • When this value is greater than the start time of the current event loop tick, it indicates that it is not ready to execute and 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 timer time nodes must not expire. At this point, the callback function of the timer phase is exited and the next phase of the event cycle tick is entered.

  • When this value is less than the start time of the current event loop tick, it indicates that at least one timer has expired. Then the loop iterates over the root node of the minimum timer heap and invokes the timer’s corresponding callback function. Each iteration of the loop updates the timer at the root of the smallest heap to the nearest time node.

Nodejs has a built-in timer

In current NodeJS, there are only two types of timers, one of which is setTimeout/setInterval. One thing to note when using setTimeout/setInterval is:

The time threshold ranges from 1 to 231-1 ms and is an integer.

In nodeJS/Node source code, both setTimeout source code implementation and setInterval source code implementation are essentially instances of the built-in Timeout class, as follows:

// Timeout values > TIMEOUT_MAX are set to 1.
const TIMEOUT_MAX = 2 ** 31 - 1

// Timer constructor function.
// The entire prototype is defined in lib/timers.js
function Timeout(callback, after, args, isRepeat, isRefed) {
  after *= 1 // Coalesce to number or NaN
  if(! (after >=1 && after <= TIMEOUT_MAX)) {
    if (after > TIMEOUT_MAX) {
      process.emitWarning(
        `${after} does not fit into` +
          ' a 32-bit signed integer.' +
          '\nTimeout duration was set to 1.'.'TimeoutOverflowWarning'
      )
    }
    after = 1 // Schedule on next tick, follows browser behavior
  }

  this._idleTimeout = after
  this._idlePrev = this
  this._idleNext = this
  this._idleStart = null
  // This must be set to null first to avoid function tracking
  Roche // On the hidden class, Revisit in V8 versions after 6.2
  this._onTimeout = nullv
  this._onTimeout = callback
  this._timerArgs = args
  this._repeat = isRepeat ? after : null
  this._destroyed = false

  if (isRefed) incRefCount()
  this[kRefed] = isRefed

  initAsyncResource(this.'Timeout')}Copy the code

As you can see from the constructor body, all timers in NodeJS are related by a two-way linked list, and all time thresholds outside the time threshold range are reset to 1ms, and all non-integer values are converted to integer values.

A common method of writing setTimeout(callback, 0) is converted to setTimeout(callback, 1) by the nodeJS internal module.

pending callbacks

The Pending Callbacks stage is used to execute the DELAYED I/O callbacks in the previous event loop tick.

Poll phase

The primary responsibilities of the POLL phase are:

  1. Calculate how long the current event loop tick needs to be blocked for processing I/O; This block indicates how long the current event loop tick should stay in the current poll phase, which is generally determined by several factors (see below) such as the minimum setTimeout/setInterval time threshold. After the blocking time is reached, it passes through the next tick phase of the current event cycle and finally enters the timer phase of the next tick event cycle, at which point the callback function of the expired timer is executed.

  2. Handle event callbacks.

As outlined above, the poll phase in NodeJS corresponds to the core logic in Libuv as follows:

timeout = 0;
/ * * * uv_backend_timeout for poll phase timeout time * http://docs.libuv.org/en/v1.x/loop.html#c.uv_backend_timeout * / (block)
if((mode == UV_RUN_ONCE && ! ran_pending) || mode == UV_RUN_DEFAULT) timeout = uv_backend_timeout(loop); uv__io_poll(loop, timeout);Copy the code

Before calling uv__io_poll, first initialize a timeout variable, which in loop mode is defined by uv_backend_timeout(loop) to determine the timeout of the poll phase, This timeout is the amount of time the poll phase should block according to the NodeJS documentation, so what is the specific basis for determining this blocking time?

int uv_backend_timeout(const uv_loop_t* loop) {
  / / https://github.com/libuv/libuv/blob/v1.35.0/src/uv-common.c#L521-L523
  // http://docs.libuv.org/en/v1.x/guide/eventloops.html#stopping-an-event-loop
  if(loop->stop_flag ! =0)
    return 0;

  if(! uv__has_active_handles(loop) && ! uv__has_active_reqs(loop))return 0;

  if(! QUEUE_EMPTY(&loop->idle_handles))return 0;

  if(! QUEUE_EMPTY(&loop->pending_queue))return 0;

  if (loop->closing_handles)
    return 0;

  return uv__next_timeout(loop);
}
Copy the code

The uv_backend_timeout function determines the blocking time of the poll phase based on some attributes of the current event loop tick:

  1. When the event loop tick is marked as stop # by the uv_stop()doc function, 0 is returned, that is, it is not blocked.

  2. Return 0 if the event loop tick is not active and there is no active request.

  3. If the idle handle queue is not empty, 0 is returned, that is, it is not blocked.

  4. When the pending Callbacks callback queue is not empty, 0 is returned, that is, it is not blocked.

  5. When there is a closing handle, that is, a close event callback, 0 is returned, that is, no blocking.

Why return 0 for no blocking and -1 for unrestricted blocking?

This is because the polling mechanism of each system platform is the key to polling in the polling stage visible from the uv__io_poll function body. 0 and -1 correspond to the polling parameters of the underlying polling mechanism of the Linux system respectively.

Taking Linux epoll mechanism as an example, the underlying epoll_wait function is called in the uv__io_poll function body to realize the core functions of Libuv polling:

nfds = epoll_wait(loop->backend_fd,
                        events,
                        ARRAY_SIZE(events),
                        timeout);
Copy the code

The parameter timeout shall specify the maximum number of milliseconds that epoll_wait() shall wait for events. If the value of this parameter is 0, then epoll_wait() shall return immediately, even if no events are available, in which case the return code shall be 0. If the value of timeout is -1, then epoll_wait() shall block until either a requested event occurs or the call is interrupted.

As you can see from the epoll_wait documentation, when the timeout parameter is 0, it returns immediately, and when the timeout parameter is -1, it blocks indefinitely until an event is triggered or the infinite blocking state is actively interrupted.

Returning to the topic of timeout, the uv__next_timeout function calculates the final poll phase blocking time without meeting the above requirement of not blocking the current event loop tick:

int uv__next_timeout(const uv_loop_t* loop) {
  const struct heap_node* heap_node;
  const uv_timer_t* handle;
  uint64_t diff;

  // The root node of the libuv timer binary minimum heap is the timer closest to the current time node of all timers
  heap_node = heap_min(timer_heap(loop));

  // The true condition is unrestricted blocking of the current poll phase
  if (heap_node == NULL)
    return - 1; /* block indefinitely */

  handle = container_of(heap_node, uv_timer_t, heap_node);

  // If the last timer is less than or equal to the time at which the current event cycle 'tick' starts
  // Then do not block and proceed to the next phase until the next 'tick' 'timer' phase executes the callback function
  if (handle->timeout <= loop->time)
    return 0;

  // As described in the Nodejs documentation, the poll phase calculates the blocking time
  // The following statement is used to calculate how long the current poll phase should block
  diff = handle->timeout - loop->time;
  // INT_MAX is declared in the limits.h header
  if (diff > INT_MAX)
    diff = INT_MAX;

  return (int) diff;
}
Copy the code

From the above function body and the previous analysis of timers, it is not difficult to see that the nearest timer execution node can be obtained by obtaining the root node of the minimum timer heap. Compare this object with the current event loop tick start time loop->time:

  1. If there are no timers, the poll phase in the current event loop tick will block indefinitely. In order to implement the I/O callback function as soon as it is added to the poll queue.

  2. If the last timer time node is less than or equal to the start time, it indicates that at least one expired timer exists in the timer binary minimum heap, and the timeout of the current poll phase is set to 0, indicating that the poll phase does not block. This is to get to the next stage as quickly as possible, which is to end the current event loop tick as quickly as possible. When the next event loop tick is entered, the timer callback function that expired in the previous tick is executed during the timer phase.

  3. If the last timer time is greater than the start time, the difference between the two timers is calculated and is not greater than the maximum value of type int. Poll blocks the current phase based on this difference in order to process asynchronous I/O events as quickly as possible during the polling phase. At this point, we can also understand that the event cycle tick always has a tendency to remain in the poll stage.

From the above source code analysis, it is not difficult to get the essence of poll stage:

  1. In order to process asynchronous I/O events as quickly as possible, the event loop tick has a tendency to maintain a poll state;

  2. How long the current poll phase should be held (blocked) is determined by whether there is a non-empty queue of callback functions and the nearest timer time node for each successive tick phase. If all queues are empty and there are no timers, the event loop remains in the POLL phase indefinitely.

Note: Because the timeout of the poll phase is calculated before the poll phase enters, the timer in the callback function queue in the current poll phase does not affect the timeout of the current poll phase.

Effect of poll on Timer

Nodejs doc:

Note: Technically, the poll phase controls when timers are executed.

Technically, the poll phase controls the timing of the timer. Why do you say so?

First of all, the event cycle of Libuv cannot be re-entered, and the event cycle always tends to remain in the poll stage, so it cannot enter the timer stage of the tick of the next event cycle without meeting the end condition of the poll stage. The callback function for the expired timer in the Timer queue cannot be executed. Hence the saying that the poll phase controls the timing of the timer callback function.

In addition, polling events unlimitlessly and calling callback functions would result in a queue of poll callback functions that would never empty at all, and the threshold detection of the timer would never happen that would drag down the entire event loop iteration. Libuv internally sets a maximum number of executions that depend on the system. Combined with the nodeJS built-in timer described above, this is one of the reasons that the timer is not guaranteed to execute the callback function exactly, but rather as quickly as possible.

The check phase

This phase is designed so that the named snippet (that is, function) can be called immediately after the poll phase ends. 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 callbacks in the check phase’s callback queue are setImmediate functions from the Poll phase.

setTimeout vs setImmediate

SetTimeout /setInterval and setImmediate Mediate Mediate Mediate Mediate Is one of only two types of timers in the NodeJS environment.

SetTimeout /setInterval is designed to call the specified callback function as soon as possible after passing a minimum time threshold. SetImmediate, however, functions as a special timer designed to give the user the chance to perform the code immediately after the poll phase (the Check phase) rather than during the Timer phase.

practice

With this brief introduction, what happens if you call setTimeout and setImmediate in user Code’s modular lexical environment?

Why is user script a modular and not a global lexical environment in NodeJS?

Console. log(this === module.exports) (not global) is simply true.

// index.js
setTimeout(
  /* setTimeoutCallback */() = > {console.log('from setTimeout')},0
)

setImmediate(
  /* setImmediateCallback */() = > {console.log('from setImmediate')})Copy the code

The above code is called with the node index.js command with unpredictable random results:

from setTimeout
from setImmediate
Copy the code

or

from setImmediate
from setTimeout
Copy the code

Why does this happen?

When the NodeJS Script is initially compiled and run, nodeJS will first take the entry JS file as the execution entry, so the execution context in the run is the Script execution context corresponding to the current entry JS file.

As mentioned above, setTimeout(callback, 0) is reset to setTimeout(callback, 1). So after the first execution of user Script code, that is, after the script execution context exits the execution context stack and starts the first event loop tick[nodeJS contributor], Will extract the nodes in the smallest heap of timer to compare whether the start time of the current event cycle tick has passed the threshold 1ms:

  • In the previous uv__run_timer(loop), if the total time of the system time call and time comparison process does not exceed 1ms, no expired timer will be found in the timer phase. SetTimeoutCallbacks also do not exist in the Timer queue. If the poll queue is empty in the poll phase, check that the check queue is not empty. Proceed to the next stage of the event loop tick and clear the setImmediateCallback callback in the Check queue registered by setImmediate. The setTimeoutCallback is added to the timer queue and executed during the current timer phase after the previous expiration timer with a threshold of 1ms is found after the subsequent event loop tick and restart.

    The console output is as follows:

    from setImmediate
    from setTimeout
    Copy the code
  • In the source code above, if the total time of system time call and time comparison exceeds 1ms, then the setTimeoutCallback of the expiration timer will be added to the Timer queue and enter the call phase of the Timer queue. The subsequent console output is as follows:

    from setTimeout
    from setImmediate
    Copy the code

As can be seen from the above analysis of the uv__run_timers function for Libuv, The use of setTimeout(callback, 0) and setImmediate(callback) does not predict the sequence of call-outs in user Script’s modular lexical environment.

  1. During the initial tick execution of the event loop, the first time check is performed.

  2. Timeout in the timer handle stores the time node after the start time of the tick of the next event cycle plus the time threshold (1ms in the example code).

  3. The interval between the time check of the initial timer and the current event cycle tick may be less than 1ms or greater than the threshold of 1ms, depending on the time of the system call, which will be affected by other applications of the operating system. When the interval is less than 1ms, the setTimeoutCallback execution in the sample code is ignored during the timer phase and the setImmediateCallback function is executed first. Instead, the setTimeoutCallback execution is performed first.

The nodeJS website also describes that in the I/O Cycle, the invocation of sample code is predictable. Why?

const fs = require('fs')

fs.readFile(__dirname, () => {
  setTimeout((a)= > {
    console.log('from setTimeout')},1)

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

The above sample code will always print:

from setImmediate
from setTimeout
Copy the code

The main advantage to using setImmediate() over setTimeout() is setImmediate() will always be executed before any timers if scheduled within an I/O cycle, independently of how many timers are present.

Based on the previous analysis, after the initial event cycle tick, all subsequent setTimeout/setInterval timer threshold checks and calls are blocked by the poll phase of the previous event cycle tick. Regardless of nodeJS or Libuv event loop abstract structure diagram or uv_run function source code, and based on the premise that the event loop cannot re-enter, the next stage of the poll phase is always the check phase, so in the I/O cycle, All timers are registered in the current event loop tick and first pass through the Check phase and subsequent phases that contain the setImmediate callback before entering the Timer phase of the next event loop tick. So much so that the setTimeout/setInterval callback registered in the I/O Cycle is always executed after the setImmediate callback in the execution order. This also explains why setImmediate is described on the NodeJS website as having a higher priority than setTimeout in an I/O cycle.

close callbacks

This stage is used to execute the callbacks for all close events. If the socket connection is suddenly closed by socket.destroy(), the close event is emitted during this phase.

Compare with the browser implementation

The biggest differences between nodeJS and the browser-side version of the Web API’s event loop are:

In NodeJS, event loops are no longer composed of a single Task queue and micro-Task queue. Instead, event loops are composed of multiple callbacks queues with multiple phases. And there is a separate FIFO queue of callback functions for each separate phase.

References

  • The Node.js Event Loop, Timers, and Process.nexttick ()
  • Official – Libuv Design
  • medium – What you should know to really understand the Node.js Event Loop
  • github issue – how the event loop works
  • github issue – non-deterministic order of execution of setTimeout vs setImmediate
  • IBM – learn nodejs the event loop
  • Jake Archibald – In the loop
  • Bert Belder (Nodejs, Libuv contributor) – Everything You Need to Know About Node.js Event Loop
  • Twitter.Bert Belder (Nodejs, Libuv contributor) – Event Loop Slider
  • Bryan Hughes, Microsoft – The Node.js Event Loop: Not So Single Threaded
  • medium – handling IO – nodejs event loop
  • Medium – Timers, Immediates and Process.nextTick – NodeJS Event Loop
  • Zhihu – NodeJS timer
  • devto – Understanding the Node.js event loop phases and how it executes the JavaScript code
  • Demystifying Asynchronous Programming Part 1: Node.js Event Loop