Node.js Internals: Lifecycle of the Event Loop

Node.js Internals: Lifecycle of the Event Loop

Explanation of how Node.js Event Loop works

Node.js is used for building highly scalable server-side applications using JavaScript. It provides,

  • Event-driven
  • Non-blocking (asynchronous) I/O
  • Cross-platform runtime environment

The event loop is the core player for maintaining these properties and makes Node.js a faster runtime.

Lifecycle

Anytime we run a node.js program, it creates one thread and then runs all of our codes inside that thread.

The event loop acts as a control structure to decide, what our one thread should be doing at one given time.

When we run a js file, Node first takes all the codes and executes them. This is the moment we enter the event loop.

Every time the event loop executes a cycle, in the Node.js world, it is called tick.

Every time the event loop is about to execute, Node quickly checks whether the loop should proceed or not for another iteration. If node decides not to run more iterations, the program closes and we go back to the terminal.

The event loop continues to the next iteration when shouldContinue method returns true.

When node first goes through the code first time, it detects pendingTimers, pendingOSTasks, and pendingOperations.

Tick

Every single iteration of an event loop is called a tick. In pseudo-code, it looks as follows

// javascript code is written inside the myFile.js

const pendingTimers = [];
const pendingOSTasks = [];
const pendingOperations = [];

// New timers, tasks, and operations are recorded from myFile
myFile.runContents();

function shouldContinue() {
  // Check one: Any pending setTimeout, setInterval, setImmediate?
  // Check two: Any pending OS tasks? 
  //    Like server listening to port, network calls
  // Check three: Any pending long-running operation?
  //    Like fs module, thread pools tasks)
  return (
    pendingTimers.length
    || pendingOSTasks.length
    || pendingOperations.length
  );
}

// Entire body executes in one 'tick'
while (shouldContinue()) {
  // 1) Node looks at pending timers and sees if any
  //     functions are ready to be called (setTimeout, setInterval)
  // 2) Node looks at pendingOSTasks and pendingOperations
  //     and calls relevant callbacks
  // 3) Node pause the execution until,
  //   - a pendingOSTasks is done
  //   - a pendingOperation is done
  //   - a timer is about to complete
  // 4) Look at pendingTimers. Call any setImmediate 
  //    (This time node does not care about setTimeout or setInterval, 
  //    it only looks at those functions, registered with setImmediate)
  // 5) Handle any 'close' events
}

Browser Perspectives

In the browser, the web API works as a javascript runtime. This web API is available in all the major browsers, like chrome, edge, opera, firefox, etc.

This handles the long-running process and lets the call stack know it is done with some process and that data is ready.

This web API handle the fetch API, dom events, and long-running process like the set-timeout or set-interval method. Even this web API can be used as caching or runtime database.

So when a long-running async process came up in the call stack, it pass that task to the web API.

Web-API handles the task in the background and put the completed task in the call-back queue.

The event loop always checks if the call stack is empty or not. If it is empty then it put the completed task from the call-back queue into the call stack.

Summary

In summary, the Node.js event loop,

  1. Process and execute code in index.js
  2. Look for pending timers, OS tasks, and pending operations. If no tasks exist, exit.
  3. Run setTimeout's, setInterval's
  4. Run callbacks for OS tasks and thread pools pending stuff
  5. Pause and wait for stuff done
  6. Run setImmediate functions
  7. Handle close events
  8. Return to step 2

References: Node JS: Advanced Concepts By Stephen Grider