阿里云主机折上折
  • 微信号
您当前的位置:网站首页 > 事件循环的阶段划分

事件循环的阶段划分

作者:陈川 阅读数:32815人阅读 分类: Node.js

事件循环的阶段划分

Node.js 的事件循环是其异步非阻塞 I/O 模型的核心,它将整个执行过程划分为多个阶段,每个阶段处理特定类型的任务。理解这些阶段的划分对于编写高效、可靠的 Node.js 应用至关重要。

定时器阶段(Timers)

定时器阶段处理由 setTimeout()setInterval() 设置的定时器回调。当事件循环进入这个阶段时,它会检查定时器队列,执行所有已经到期的定时器回调。

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

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

在这个例子中,setTimeout 的回调会在定时器阶段执行,而 setImmediate 的回调会在检查阶段执行。由于定时器阶段先于检查阶段,所以通常 Timeout 1 会先于 Immediate 1 输出。

待定回调阶段(Pending Callbacks)

这个阶段执行一些系统操作的回调,比如 TCP 错误。例如,如果 TCP 套接字在尝试连接时收到 ECONNREFUSED,这些错误的回调就会在这个阶段执行。

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

socket.on('error', (err) => {
  console.log('Error:', err.message); // 这个回调会在待定回调阶段执行
});

闲置/准备阶段(Idle, Prepare)

这是事件循环的内部阶段,通常开发者不需要直接与之交互。Node.js 在这个阶段进行一些内部准备工作。

轮询阶段(Poll)

轮询阶段有两个主要功能:

  1. 计算应该阻塞和轮询 I/O 的时间
  2. 处理轮询队列中的事件
const fs = require('fs');

fs.readFile('/path/to/file', (err, data) => {
  console.log('File read complete'); // 这个回调会在轮询阶段执行
});

// 模拟长时间运行的操作
setTimeout(() => {
  console.log('Timeout in poll phase');
}, 100);

如果轮询队列不为空,事件循环会遍历队列并同步执行回调,直到队列耗尽或达到系统限制。如果队列为空,事件循环会检查是否有定时器即将到期,如果有则进入定时器阶段。

检查阶段(Check)

这个阶段专门处理 setImmediate() 设置的回调。如果轮询阶段空闲且 setImmediate 回调被排队,事件循环会立即进入检查阶段而不是等待轮询事件。

setImmediate(() => {
  console.log('Immediate callback'); // 在检查阶段执行
});

fs.readFile('/path/to/file', () => {
  setImmediate(() => {
    console.log('Immediate inside readFile'); // 也会在检查阶段执行
  });
});

关闭回调阶段(Close Callbacks)

这个阶段处理一些关闭事件的回调,比如 socket.on('close', ...)

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

server.on('connection', (socket) => {
  socket.on('close', () => {
    console.log('Socket closed'); // 这个回调会在关闭回调阶段执行
  });
});

server.listen(3000);

阶段执行顺序示例

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'));

这个示例展示了不同阶段回调的执行顺序:

  1. 微任务(nextTickpromise)最先执行
  2. 然后是定时器阶段或检查阶段(取决于事件循环的启动状态)
  3. I/O 回调后的 setImmediate 会在同一轮循环的检查阶段执行

微任务队列

虽然不属于事件循环的主要阶段,但微任务(process.nextTickPromise 回调)在每个阶段结束后都会立即执行。

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);

在这个例子中,微任务会在每个定时器回调执行后立即处理,而不是等到整个定时器阶段结束。

阶段转换的实际应用

理解阶段划分有助于优化应用性能。例如,在需要尽快执行某些操作时,可以使用 setImmediate 而不是 setTimeout(fn, 0),因为前者会在检查阶段执行,而后者要等到下一个定时器阶段。

function heavyComputation(callback) {
  // 使用 setImmediate 分解长时间运行的任务
  setImmediate(() => {
    // 第一阶段计算
    setImmediate(() => {
      // 第二阶段计算
      callback();
    });
  });
}

事件循环阶段的底层实现

Node.js 的事件循环构建在 libuv 库之上。libuv 使用不同的系统机制(如 epoll、kqueue、IOCP 等)来实现跨平台的事件通知系统。每个阶段对应 libuv 不同的处理逻辑:

  • 定时器阶段使用最小堆数据结构来高效管理定时器
  • 轮询阶段使用操作系统提供的 I/O 多路复用机制
  • 检查阶段维护了一个简单的回调队列

阶段超时和轮询优化

事件循环在每个阶段都会计算应该等待的时间。这个计算基于:

  1. 是否有待处理的 setImmediate 回调
  2. 是否有即将到期的定时器
  3. 当前是否有活跃的异步操作
const start = Date.now();

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

// 模拟长时间同步操作
while (Date.now() - start < 200) {}

在这个例子中,虽然定时器设置为 100ms,但由于同步操作阻塞了事件循环,实际执行时间会远超过 100ms。

多阶段协作示例

一个完整的 HTTP 服务器示例展示了多个阶段的协作:

const http = require('http');

const server = http.createServer((req, res) => {
  // 这个回调在轮询阶段执行
  setImmediate(() => {
    // 在检查阶段执行
    res.writeHead(200);
    
    setTimeout(() => {
      // 在下一个循环的定时器阶段执行
      res.end('Hello World');
    }, 0);
  });
});

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

阶段与性能监控

Node.js 的性能监控工具(如 perf_hooks)可以帮助观察事件循环各阶段的耗时:

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

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

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

阶段阻塞的识别和处理

长时间阻塞某个阶段会导致应用响应变慢。常见的阻塞场景包括:

  • 定时器阶段:大量定时器回调执行
  • 轮询阶段:同步文件操作或 CPU 密集型计算
  • 检查阶段:复杂的 setImmediate 回调链
// 识别阶段阻塞的示例
setInterval(() => {
  const start = process.hrtime();
  setImmediate(() => {
    const delta = process.hrtime(start);
    console.log(`Check phase delay: ${delta[0] * 1e3 + delta[1] / 1e6}ms`);
  });
}, 1000);

跨阶段错误处理

错误处理需要考虑不同阶段的上下文:

// 跨阶段错误处理示例
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);
});

阶段优先级的实际应用

理解阶段优先级有助于设计高效的系统:

// 使用 process.nextTick 确保回调在其他I/O前执行
function apiCall(arg, callback) {
  if (typeof arg !== 'string') {
    process.nextTick(() => callback(new TypeError('argument should be string')));
    return;
  }
  // 正常处理...
}

事件循环阶段的调试

Node.js 提供了多种调试事件循环阶段的方法:

// 使用 async_hooks 跟踪异步操作
const async_hooks = require('async_hooks');

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

阶段与工作线程

在工作线程中,事件循环的运作方式与主线程类似,但每个线程有自己的事件循环:

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

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

浏览器与Node.js事件循环差异

虽然都称为事件循环,但浏览器环境与Node.js的实现有显著区别:

// 浏览器中的微任务执行时机不同
// 这段代码在浏览器和Node.js中可能有不同的输出顺序
setTimeout(() => console.log('timeout'), 0);

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

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

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

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

前端川

前端川,陈川的代码茶馆🍵,专治各种不服的Bug退散符💻,日常贩卖秃头警告级的开发心得🛠️,附赠一行代码笑十年的摸鱼宝典🐟,偶尔掉落咖啡杯里泡开的像素级浪漫☕。‌