阿里云主机折上折
  • 微信号
Current Site:Index > The division of phases in the event loop

The division of phases in the event loop

Author:Chuan Chen 阅读数:32814人阅读 分类: Node.js

Phases of the Event Loop

The event loop in Node.js is the core of its asynchronous, non-blocking I/O model. It divides the entire execution process into multiple phases, each handling specific types of tasks. Understanding these phases is crucial for writing efficient and reliable Node.js applications.

Timer Phase (Timers)

The timer phase handles callbacks set by setTimeout() and setInterval(). When the event loop enters this phase, it checks the timer queue and executes all expired timer callbacks.

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

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

In this example, the setTimeout callback executes during the timer phase, while the setImmediate callback executes during the check phase. Since the timer phase precedes the check phase, Timeout 1 is typically logged before Immediate 1.

Pending Callbacks Phase

This phase executes callbacks for certain system operations, such as TCP errors. For example, if a TCP socket receives ECONNREFUSED while attempting to connect, the error callback is executed in this phase.

const net = require('net');
const socket = net.connect(12345);

socket.on('error', (err) => {
  console.log('Error:', err.message); // This callback executes in the pending callbacks phase
});

Idle/Prepare Phase

This is an internal phase of the event loop, and developers typically do not interact with it directly. Node.js performs internal preparations during this phase.

Poll Phase

The poll phase has two main functions:

  1. Calculating how long it should block and poll for I/O
  2. Processing events in the poll queue
const fs = require('fs');

fs.readFile('/path/to/file', (err, data) => {
  console.log('File read complete'); // This callback executes in the poll phase
});

// Simulate a long-running operation
setTimeout(() => {
  console.log('Timeout in poll phase');
}, 100);

If the poll queue is not empty, the event loop iterates through the queue and executes callbacks synchronously until the queue is exhausted or the system limit is reached. If the queue is empty, the event loop checks for any timers about to expire and proceeds to the timer phase if necessary.

Check Phase

This phase specifically handles callbacks set by setImmediate(). If the poll phase is idle and setImmediate callbacks are queued, the event loop immediately proceeds to the check phase instead of waiting for poll events.

setImmediate(() => {
  console.log('Immediate callback'); // Executes in the check phase
});

fs.readFile('/path/to/file', () => {
  setImmediate(() => {
    console.log('Immediate inside readFile'); // Also executes in the check phase
  });
});

Close Callbacks Phase

This phase handles callbacks for close events, such as socket.on('close', ...).

const server = require('net').createServer();

server.on('connection', (socket) => {
  socket.on('close', () => {
    console.log('Socket closed'); // This callback executes in the close callbacks phase
  });
});

server.listen(3000);

Example of Phase Execution Order

setTimeout(() => console.log('timeout'), 0);
setImmediate(() => console.log('immediate'));

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

process.nextTick(() => console.log('nextTick'));
Promise.resolve().then(() => console.log('promise'));

This example demonstrates the execution order of callbacks across different phases:

  1. Microtasks (nextTick and promise) execute first
  2. Followed by the timer phase or check phase (depending on the event loop's startup state)
  3. setImmediate callbacks after I/O execute in the check phase of the same loop iteration

Microtask Queue

Although not part of the main event loop phases, microtasks (process.nextTick and Promise callbacks) execute immediately after each phase completes.

setTimeout(() => {
  console.log('timeout 1');
  Promise.resolve().then(() => console.log('promise 1'));
}, 0);

setTimeout(() => {
  console.log('timeout 2');
  process.nextTick(() => console.log('nextTick 1'));
}, 0);

In this example, microtasks are processed immediately after each timer callback executes, rather than waiting for the entire timer phase to complete.

Practical Applications of Phase Transitions

Understanding phase division helps optimize application performance. For example, when certain operations need to execute as soon as possible, setImmediate can be used instead of setTimeout(fn, 0), as the former executes in the check phase while the latter waits for the next timer phase.

function heavyComputation(callback) {
  // Use setImmediate to break up long-running tasks
  setImmediate(() => {
    // First phase of computation
    setImmediate(() => {
      // Second phase of computation
      callback();
    });
  });
}

Underlying Implementation of Event Loop Phases

Node.js's event loop is built on the libuv library. Libuv uses different system mechanisms (e.g., epoll, kqueue, IOCP) to implement a cross-platform event notification system. Each phase corresponds to different processing logic in libuv:

  • The timer phase uses a min-heap data structure to efficiently manage timers
  • The poll phase uses OS-provided I/O multiplexing mechanisms
  • The check phase maintains a simple callback queue

Phase Timeouts and Poll Optimization

The event loop calculates the waiting time for each phase based on:

  1. Whether there are pending setImmediate callbacks
  2. Whether there are timers about to expire
  3. Whether there are active asynchronous operations
const start = Date.now();

setTimeout(() => {
  console.log(`Timed out after ${Date.now() - start}ms`);
}, 100);

// Simulate a long synchronous operation
while (Date.now() - start < 200) {}

In this example, although the timer is set for 100ms, the synchronous operation blocks the event loop, causing the actual execution time to far exceed 100ms.

Multi-Phase Collaboration Example

A complete HTTP server example demonstrates collaboration across multiple phases:

const http = require('http');

const server = http.createServer((req, res) => {
  // This callback executes in the poll phase
  setImmediate(() => {
    // Executes in the check phase
    res.writeHead(200);
    
    setTimeout(() => {
      // Executes in the timer phase of the next iteration
      res.end('Hello World');
    }, 0);
  });
});

server.listen(3000, () => {
  console.log('Server running');
});

Phases and Performance Monitoring

Node.js performance monitoring tools (e.g., perf_hooks) can help observe the time consumption of each event loop phase:

const { monitorEventLoopDelay } = require('perf_hooks');

const histogram = monitorEventLoopDelay();
histogram.enable();

setInterval(() => {
  console.log(`Event loop delay: ${histogram.mean / 1e6}ms`);
  histogram.reset();
}, 1000);

Identifying and Handling Phase Blocking

Long blocking in any phase can slow down application responsiveness. Common blocking scenarios include:

  • Timer phase: Execution of a large number of timer callbacks
  • Poll phase: Synchronous file operations or CPU-intensive computations
  • Check phase: Complex setImmediate callback chains
// Example of identifying phase blocking
setInterval(() => {
  const start = process.hrtime();
  setImmediate(() => {
    const delta = process.hrtime(start);
    console.log(`Check phase delay: ${delta[0] * 1e3 + delta[1] / 1e6}ms`);
  });
}, 1000);

Cross-Phase Error Handling

Error handling must consider the context of different phases:

// Example of cross-phase error handling
process.on('uncaughtException', (err) => {
  console.error('Global error:', err);
});

setTimeout(() => {
  throw new Error('Timer error');
}, 0);

setImmediate(() => {
  throw new Error('Immediate error');
});

fs.readFile('nonexistent', (err) => {
  if (err) console.error('I/O error:', err.message);
});

Practical Applications of Phase Priority

Understanding phase priority helps design efficient systems:

// Use process.nextTick to ensure callbacks execute before other I/O
function apiCall(arg, callback) {
  if (typeof arg !== 'string') {
    process.nextTick(() => callback(new TypeError('argument should be string')));
    return;
  }
  // Normal processing...
}

Debugging Event Loop Phases

Node.js provides various methods for debugging event loop phases:

// Use async_hooks to track asynchronous operations
const async_hooks = require('async_hooks');

const hook = async_hooks.createHook({
  init(asyncId, type, triggerAsyncId) {
    console.log(`${type} created`);
  }
});
hook.enable();

Phases and Worker Threads

In worker threads, the event loop operates similarly to the main thread, but each thread has its own event loop:

const { Worker } = require('worker_threads');

new Worker(`
  const { parentPort } = require('worker_threads');
  setImmediate(() => {
    parentPort.postMessage('from worker immediate');
  });
`, { eval: true }).on('message', console.log);

Differences Between Browser and Node.js Event Loops

Although both are called event loops, browser environments and Node.js implementations differ significantly:

// Microtasks execute at different times in browsers
// This code may produce different output orders in browsers and Node.js
setTimeout(() => console.log('timeout'), 0);

Promise.resolve().then(() => console.log('promise'));

requestAnimationFrame(() => console.log('raf'));

本站部分内容来自互联网,一切版权均归源网站或源作者所有。

如果侵犯了你的权益请来信告知我们删除。邮箱:cc@cccx.cn

Front End Chuan

Front End Chuan, Chen Chuan's Code Teahouse 🍵, specializing in exorcising all kinds of stubborn bugs 💻. Daily serving baldness-warning-level development insights 🛠️, with a bonus of one-liners that'll make you laugh for ten years 🐟. Occasionally drops pixel-perfect romance brewed in a coffee cup ☕.