Imagine you’re ordering coffee at a café. If the barista only focuses on one customer and waits for them to finish before taking the next order, the shop’s efficiency would be very low. But if the barista can “handle multiple customers at once”—taking orders first, then preparing coffee, and addressing other customers’ needs in between—service speeds up dramatically. Node.js processes requests similarly to this clever barista, with the Event Loop as its core mechanism.

I. Single-Threaded & Non-Blocking: Node.js’s “Speed Secret”

First, clarify: Node.js is single-threaded. This means only one piece of code executes at a time, unlike multi-threaded programs that can run multiple tasks simultaneously. However, this doesn’t make Node.js “slow”—in fact, it efficiently handles massive concurrent requests through non-blocking I/O and collaboration with the Event Loop.

For example, a traditional server (e.g., Java’s Tomcat) processing a “file read” operation would “block” the program until the file is fully read before handling other requests. Node.js avoids this: when you call fs.readFile (file reading), Node.js offloads the task to the underlying libuv library (an asynchronous I/O utility) and immediately processes the next request. Once the file is read, libuv places the “read complete” message (a callback function) into a task queue. The Event Loop “polls” this queue, extracting tasks to execute.

II. Event Loop: How to “Handle Multiple Tasks Simultaneously”?

The Event Loop acts as a “dispatcher,” processing asynchronous operations in task queues in a stage-by-stage manner. Each stage handles a specific type of asynchronous task, executing them in a fixed order until all tasks are complete.

Core Stages (Simplified for Beginners):

  1. Timers: Processes setTimeout and setInterval callbacks. For example, setTimeout(fn, 1000) queues fn for execution after 1 second.

  2. Pending Callbacks: Handles system-level callbacks (e.g., TCP errors, DNS lookups). Rarely used in daily development.

  3. Idle/Prepare: Internal Node.js optimizations; not critical for most use cases.

  4. Poll: The most critical stage! Waits for I/O events (e.g., file read completion, network response) and executes corresponding callbacks. If no I/O events exist, it “blocks” here to wait for new events.

  5. Check: Processes setImmediate callbacks (executes immediately after the current poll phase).

  6. Close Callbacks: Handles close event callbacks (e.g., socket.on('close')).

III. Asynchronous Model: Call Stack, Task Queue, and Event Loop

To understand the Event Loop, master JavaScript’s asynchronous execution model:

  • Call Stack: The “stage” for synchronous code execution. For example, console.log('A') enters the stack, executes, and then pops out.

  • Asynchronous Tasks: When an asynchronous operation (e.g., setTimeout, fs.readFile) is encountered, it is offloaded to libuv and does not block the call stack.

  • Task Queue: After asynchronous operations complete, their callback functions are added to the queue (e.g., setTimeout callbacks go to the Timers queue, fs.readFile callbacks to the Poll queue).

  • Event Loop: Continuously checks task queues, extracting tasks in stage order and executing them in the call stack until all queues are empty.

IV. Why This Design Makes Node.js So Fast?

  1. Non-Blocking I/O: Avoids waiting for I/O operations (e.g., file reads, network requests). While waiting, the CPU processes other requests. For example, if User A’s request is waiting for a database response, Node.js can handle User B’s request without idle time.

  2. Efficient Callback Scheduling: The Event Loop processes tasks in fixed stages/queues, avoiding multi-thread context-switching overhead (which is slower due to frequent context saving/restoring).

  3. Single-Threaded + Asynchronous: Despite being single-threaded, asynchronous callbacks enable “concurrency.” For example, with 1000 simultaneous user requests, the Event Loop processes each user’s asynchronous tasks sequentially—efficiently responding as long as no blocking occurs.

V. Practical Example: How the Event Loop Works

console.log('Synchronous code starts');

// 1. Timer callback (Timers stage)
setTimeout(() => {
  console.log('Timer callback (setTimeout)');
}, 0);

// 2. Immediate callback (Check stage)
setImmediate(() => {
  console.log('Immediate callback (setImmediate)');
});

// 3. File read callback (Poll stage)
const fs = require('fs');
fs.readFile('test.txt', (err, data) => {
  console.log('File read callback');
});

console.log('Synchronous code ends');

Execution Order:
1. Execute all synchronous code first: Outputs Synchronous code startsSynchronous code ends.
2. Event Loop enters the Timers stage, processing setTimeout (delays are minimal, even if 0): Outputs Timer callback (setTimeout).
3. Enter the Poll stage: The readFile callback triggers (assuming test.txt exists), outputting File read callback.
4. Finally, the Check stage processes setImmediate: Outputs Immediate callback (setImmediate).

(Note: Actual order may vary slightly by environment, but the core is stage-wise processing.)

VI. Summary

The Event Loop is Node.js’s “heart,” enabling single-threaded JavaScript to handle high concurrency via non-blocking I/O and stage-based task scheduling. Remember: asynchronous code execution order is determined by the Event Loop. Mastering its workflow helps write efficient Node.js code. Next time you use Node.js for heavy workloads, recall this clever barista and the Event Loop driving its performance!

Xiaoye