What is an event loop

Event loops are node.js’ mechanism for handling non-blocking I/O operations — even though JavaScript is single-threaded — and they move operations to the system kernel when possible.

Since most kernels today are multithreaded, they can handle multiple operations in the background. When one of these operations completes, the kernel tells Node.js to add the appropriate callback function to the polling queue for execution. We’ll cover this in more detail later in this article.

Event loop mechanism parsing

When Node.js starts, it initialises the event loop, processes the supplied input script (or dumps it into the REPL, which is not covered in this article), and it may call some asynchronous API, schedule the timer, or call process.Nexttick () to start processing the event loop.

The diagram below shows a simplified overview of the operation sequence of the event loop.

┌ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ┐ ┌ ─ > │ timers │ │ └ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ┬ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ┘ │ ┌ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ┴ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ┐ │ │ pending Callbacks │ │ └ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ┬ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ┘ │ ┌ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ┴ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ┐ │ │ idle, Prepare │ │ └ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ┬ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ┘ ┌ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ┐ │ ┌ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ┴ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ┐ │ incoming: │ │ │ poll │ < ─ ─ ─ ─ ─ ┤ connections, │ │ └ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ┬ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ┘ │ data, Etc. │ │ ┌ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ┴ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ┐ └ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ┘ │ │ check │ │ └ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ┬ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ┘ │ ┌ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ┴ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ┐ └ ─ ─ ┤ close callbacks │ └ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ┘Copy the code

Note: Each box is called a phase of the event loop mechanism.

Each phase has a FIFO queue to perform the callback. While each phase is special, typically, when an event loop enters a given phase, it will perform any operation specific to that phase and then execute the callbacks in that phase’s queue until the queue is exhausted or the maximum number of callbacks has been executed. When the queue is exhausted or the callback limit is reached, the event loop moves to the next phase, and so on.

Since any of these operations can schedule _ more _ operations and new events that are queued by the kernel during the polling phase, polling events can be queued while the polling events are being processed. Therefore, a long-running callback can allow the polling phase to run longer than the timer’s threshold time. See the Timers and Polling section for more information.

Note:There are subtle differences between Windows and Unix/Linux implementations, but that’s not important for the demo. The most important part is here. There are actually seven or eight steps, but we’re concerned that Node.js actually uses some of these steps.

Summary of stage

  • The timer: This phase has been executedsetTimeout()setInterval()The scheduling callback function of.
  • Pending callback: An I/O callback that is deferred until the next iteration of the loop.
  • Idle, prepare: used only in the system.
  • polling: Retrieves new I/O events; Perform I/ O-related callbacks (in almost all cases, except for closed callback functions, those made by timers andsetImmediate()Out of schedule), the rest of the case node will block at this point in due course.
  • detection:setImmediate()This is where the callback function is executed.
  • The closed callback function: Some closed callback functions such as:socket.on('close', ...).

Between each running event loop, Node.js checks to see if it is waiting for any asynchronous I/O or timer, and shuts it down completely if it isn’t.

Detailed overview of phases

The timer

The timer specifies the threshold at which the provided callback can be executed, not the exact time the user expects it to be executed. After a specified interval, timer callbacks are run as early as possible. However, operating system scheduling or other running callbacks can delay them.

Pay attention to:pollingphaseControls when the timer is executed.

For example, suppose you schedule a timer that expires after 100 milliseconds, and then your script starts asynchronously reading a file that will take 95 milliseconds:

const fs = require('fs');

function someAsyncOperation(callback) {
  // Assume this takes 95ms to complete
  fs.readFile('/path/to/file', callback);
}

const timeoutScheduled = Date.now();

setTimeout(() => {
  const delay = Date.now() - timeoutScheduled;

  console.log(`${delay}ms have passed since I was scheduled`);
}, 100);

// do someAsyncOperation which takes 95 ms to complete
someAsyncOperation(() => {
  const startCallback = Date.now();

  // do something that will take 10ms...
  while (Date.now() - startCallback < 10) {
    // do nothing
  }
});
Copy the code

When the event loop enters the polling phase, it has an empty queue (fs.readfile () is not yet complete at this point), so it will wait for the remaining milliseconds until the fastest one of the timer thresholds is reached. When it waits 95 milliseconds for fs.readfile () to finish reading the file, its callback, which takes 10 milliseconds to complete, is added to the polling queue and executed. When the callback is complete, there are no more callbacks in the queue, so the event loop mechanism looks at the timer that reached the threshold the fastest and then goes back to the timer phase to execute the timer callback. In this example, you will see that the total delay between scheduling the timer and its callback being executed will be 105 milliseconds.

Note: To prevent the polling phase from starving the event loop, Libuv (the C library that implements node.js event loops and all of the platform’s asynchronous behavior) also has a hard maximum (system dependent) before stopping the polling to get more events.

The suspended callback function

This phase performs callbacks to certain system operations, such as TCP error types. For example, some * NIx systems want to wait to report an error if a TCP socket receives ECONNREFUSED while trying to connect. This will be queued for execution in the pending callback phase.

polling

The polling phase has two important functions:

  1. Calculate the time when I/O should be blocked and polled.
  2. Then, the events in the polling queue are processed.

When the event loop enters the polling phase and there is no scheduled timer, one of two things happens:

  • If the polling queue is not empty

    , the event loop will loop through the callback queue and execute them synchronously until the queue is exhausted or a system-specific hard limit is reached.

  • If the polling queue is empty, two more things happen:

    • If the script is dispatched by setImmediate(), the event loop ends the polling phase and continues the checking phase to execute those dispatched scripts.

    • If the script is not dispatched by setImmediate(), the event loop waits for the callback to be added to the queue and then executes immediately.

Once the polling queue is empty, the event loop checks for timers whose time threshold has been reached. If one or more timers are ready, the event loop loops back to the timer phase to execute the callbacks for those timers.

The check phase

This phase allows people to perform callbacks immediately after the polling phase is complete. If the polling phase becomes idle and the script is queued after using setImmediate(), the event loop may continue to the check phase instead of waiting.

SetImmediate () is actually a special timer that runs during a separate phase of the event loop. It uses a Libuv API to schedule callbacks to be executed after the polling phase is complete.

Typically, when executing code, the event loop eventually hits the polling phase, where it waits for incoming connections, requests, and so on. However, if the callback has already been scheduled using setImmediate() and the polling phase becomes idle, it will end the phase and continue to the check phase rather than continue waiting for the polling event.

The closed callback function

If a socket or handler is suddenly closed (such as socket.destroy()), the ‘close’ event is emitted at this stage. Otherwise it will be issued through process.nexttick ().

SetImmediate contrast setTimeout () ()

SetImmediate () is similar to setTimeout(), but depending on the timing of the call, they behave differently.

  • setImmediate()Designed once in the presentpollingWhen the phase is complete, the script is executed.
  • setTimeout()Run the script after the minimum threshold in ms.

The order in which timers are executed will vary depending on the context in which they are invoked. If both are called from within the main module, the timer is constrained by process performance (which may be affected by other running applications on the computer).

For example, if you run the following script that is not in the I/O cycle (that is, the main module), the order in which the two timers are executed is nondeterministic because it is constrained by process performance:

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

setImmediate(() => {
  console.log('immediate');
});


$ node timeout_vs_immediate.js
timeout
immediate

$ node timeout_vs_immediate.js
immediate
timeout
Copy the code

However, if you call the two functions inside an I/O loop, setImmediate is always called preferentially:

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

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


$ node timeout_vs_immediate.js
immediate
timeout

$ node timeout_vs_immediate.js
immediate
timeout
Copy the code

The main advantage of using setImmediate() over setTimeout() is that, if setImmediate() is mediate during an I/O cycle, it will run before any of the timers, regardless of how many timers exist

process.nextTick()

To understand the process. NextTick ()

You may have noticed that process.nexttick () is not shown in the diagram, even though it is part of the asynchronous API. This is because process.nexttick () is technically not part of the event loop. Instead, it will process the nextTickQueue after the current operation completes, regardless of the current phase of the event loop. Here an _ operation _ is seen as a transition from the underlying C/C++ processor and handles the JavaScript code that needs to be executed.

Recalling our diagram, any time process.nexttick () is called during a given phase, all callbacks passed to process.nexttick () are resolved before the event loop continues. This can cause some bad things, because it allows you to starve your I/O through recursive process.nexttick () calls, preventing the event loop from reaching the polling phase.

Why is this allowed?

Why is something like this included in Node.js? Part of it is a design philosophy in which the API should always be asynchronous, even if it doesn’t have to be. Take this code snippet as an example:

function apiCall(arg, callback) { if (typeof arg ! == 'string') return process.nextTick( callback, new TypeError('argument should be string') ); }Copy the code

Code snippets for parameter checking. If not, the error is passed to the callback function. A recent update to the API allows you to pass parameters to process.nexttick (), which will allow it to accept any parameters after the callback function location and pass the parameters to the callback function as arguments to the callback function, so you don’t have to nest the functions.

What we are doing is passing the error back to the user, but only after executing the rest of the user’s code. By using process.nexttick (), we ensure that apiCall() always executes its callback _ after the rest of the user code and _ before letting the event loop continue. To achieve this, the JS call stack is allowed to expand and then execute the provided callback immediately, allowing recursive calls to process.nexttick () without touching RangeError: exceeding V8’s maximum call stack size limit.

This design principle can lead to some potential problems. Take this code snippet as an example:

let bar;

// this has an asynchronous signature, but calls callback synchronously
function someAsyncApiCall(callback) {
  callback();
}

// the callback is called before `someAsyncApiCall` completes.
someAsyncApiCall(() => {
  // since someAsyncApiCall has completed, bar hasn't been assigned any value
  console.log('bar', bar); // undefined
});

bar = 1;
Copy the code

Users define someAsyncApiCall() as having an asynchronous signature, but it actually runs synchronously. When it is called, the callback provided to someAsyncApiCall() is called within the same phase of the event loop, because someAsyncApiCall() doesn’t actually do anything asynchronously. As a result, the callback function is trying to reference bar, but the variable may not be in scope yet because the script has not finished running.

By placing callbacks in process.nexttick (), the script still has the ability to run complete, allowing all variables, functions, and so on to be initialized before the callback is called. It also has the advantage of not letting the event loop continue, and is suitable for warning the user of an error before the event loop continues. Here is the previous example using process.nexttick () :

let bar;

function someAsyncApiCall(callback) {
  process.nextTick(callback);
}

someAsyncApiCall(() => {
  console.log('bar', bar); // 1
});

bar = 1;
Copy the code

Here’s another real example:

const server = net.createServer(() => {}).listen(8080);

server.on('listening', () => {});
Copy the code

Ports are bound immediately only if they are passed. Therefore, the ‘listening’ callback can be called immediately. The problem is that the.on(‘listening’) callback has not been set at that point in time.

To get around this problem, the ‘listening’ event is placed in nextTick() to allow the script to complete. This lets the user set up whatever event handlers they want.

Process. NextTick () contrast setImmediate ()

As far as the user is concerned, we have two similar calls, but their names are confusing.

  • process.nextTick()Execute immediately at the same stage.
  • setImmediate()Fired on the next iteration or ‘tick’ of the event loop.

Essentially, the two names should be swapped because process.nexttick () triggers faster than setImmediate(), but this is a hangover from the past and therefore unlikely to change. A rash name swap would break most packages on NPM. More modules are being added every day, which means we have to wait every day for more potential damage to happen. As confusing as these names are, they don’t change.

We recommend that developers use setImmediate() in all situations because it is easier to understand.

Why use process.nexttick ()?

There are two main reasons:

  1. Allows users to handle errors, clean up any unwanted resources, or retry requests before the event loop continues.

  2. It is sometimes necessary to have the callback run after the stack has been unwound, but before the event loop continues.

Here is a simple example of what users expect:

const server = net.createServer();
server.on('connection', (conn) => {});

server.listen(8080);
server.on('listening', () => {});
Copy the code

Suppose listen() runs at the start of the event loop, but the listening callback is placed in setImmediate(). It is not immediately bound to a port unless the host name has been passed. In order for the event loop to continue, it must hit the polling phase, which means it is possible that a connection was received and the connection event was fired before it could be listened for.

Another example runs a function constructor that inherits from EventEmitter and wants to call the constructor:

const EventEmitter = require('events'); const util = require('util'); function MyEmitter() { EventEmitter.call(this); this.emit('event'); } util.inherits(MyEmitter, EventEmitter); const myEmitter = new MyEmitter(); myEmitter.on('event', () => { console.log('an event occurred! '); });Copy the code

You cannot immediately fire the event from the constructor because the script has not yet been processed to the point where the user assigns a callback function to the event. Therefore, process.nexttick () can be used in the constructor itself to set the callback so that the event is emitted after the constructor completes, which is the expected result:

const EventEmitter = require('events'); const util = require('util'); function MyEmitter() { EventEmitter.call(this); // use nextTick to emit the event once a handler is assigned process.nextTick(() => { this.emit('event'); }); } util.inherits(MyEmitter, EventEmitter); const myEmitter = new MyEmitter(); myEmitter.on('event', () => { console.log('an event occurred! '); });Copy the code

Source: nodejs.org/en/docs/gui…