Node.js(五)事件机制和回调

一、事件机制(Event Loop & EventEmitter)

1.1 什么是事件机制?

Node.js 基于事件驱动和异步非阻塞 I/O。它通过**事件循环(Event Loop)**来调度任务,保证高并发性能。

  • 事件循环:主线程不断轮询任务队列,处理异步事件和回调。
  • 事件触发:异步操作完成后,将回调函数加入事件队列,等待主线程处理。

1.2 EventEmitter

Node.js 提供了 events 模块,核心类是 EventEmitter,用于事件的发布与订阅。

示例:自定义事件

const EventEmitter = require('events');
const emitter = new EventEmitter();

// 监听事件
emitter.on('greet', (name) => {
  console.log(`Hello, ${name}!`);
});

// 触发事件
emitter.emit('greet', 'Node.js');

常用方法

  • on(eventName, listener):注册事件监听
  • emit(eventName, ...args):触发事件
  • once(eventName, listener):只监听一次
  • removeListener(eventName, listener):移除监听

应用场景

  • 网络连接、文件读取、定时器等系统事件
  • 用户自定义业务事件(如消息队列、任务调度)

二、回调(Callback)

2.1 什么是回调?

回调是函数作为参数传递,在异步操作完成后执行。Node.js 早期大量采用回调模式。

示例:异步读取文件

const fs = require('fs');

fs.readFile('example.txt', 'utf8', (err, data) => {
  if (err) {
    console.error('读取失败:', err);
    return;
  }
  console.log('文件内容:', data);
});

2.2 回调地狱(Callback Hell)

多层嵌套回调导致代码难以维护:

fs.readFile('a.txt', 'utf8', (err, data) => {
  if (err) return;
  fs.readFile('b.txt', 'utf8', (err, data2) => {
    if (err) return;
    // ...
  });
});

2.3 优化方式

  • Promise:链式调用,减少嵌套
  • async/await:同步风格书写异步代码
  • 事件机制:用事件管理复杂异步流程

三、事件机制与回调的关系

  • Node.js 的异步 API(如 fs、net、http)底层都基于事件机制,最终通过回调通知结果。
  • 事件机制适合多次触发(如 socket 数据),回调适合一次性操作(如文件读取)。

四、进阶:自定义异步流程管理

4.1 用事件机制管理异步流程

const EventEmitter = require('events');
const emitter = new EventEmitter();

emitter.on('step1', () => {
  setTimeout(() => {
    console.log('第1步完成');
    emitter.emit('step2');
  }, 1000);
});

emitter.on('step2', () => {
  setTimeout(() => {
    console.log('第2步完成');
    emitter.emit('done');
  }, 1000);
});

emitter.on('done', () => {
  console.log('所有步骤完成');
});

emitter.emit('step1');

4.2 用 Promise/async 优化回调地狱

const fs = require('fs/promises');

async function readFiles() {
  const data1 = await fs.readFile('a.txt', 'utf8');
  const data2 = await fs.readFile('b.txt', 'utf8');
  console.log(data1, data2);
}

readFiles();

五、实践建议

  • 简单异步用回调,复杂流程用事件机制或 Promise/async。
  • 注意回调中的错误处理,避免程序崩溃。
  • 事件机制适合模块间解耦、扩展性强的场景。

六、事件循环原理、自定义事件模块、异步流程设计

1、事件循环原理(Event Loop)

1.1 Node.js 事件循环简介

事件循环是 Node.js 异步非阻塞 I/O 的核心。它让 Node.js 可以高效处理大量并发请求。

基本流程:

  1. 执行主线程代码(同步代码)。
  2. 检查事件队列(包括回调、定时器、I/O 等)。
  3. 依次处理队列中的任务。
  4. 新的异步任务加入队列,等待事件循环调度。
  5. 循环往复,直到没有任务。

1.2 阶段划分(简化版)

Node.js 事件循环主要分为以下几个阶段:

  • timers:处理 setTimeout/setInterval 等定时器回调。
  • pending callbacks:处理一些系统操作的回调。
  • idle, prepare:内部使用。
  • poll:处理 I/O 事件(文件、网络等),如果无事件则可能进入下一个阶段。
  • check:处理 setImmediate 回调。
  • close callbacks:处理如 socket 的 close 事件。

事件循环示例

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

console.log('main');

输出顺序:

main
nextTick
timeout
immediate

process.nextTick 总是优先于定时器和 setImmediate 执行。

1.3 参考资料


2、自定义事件模块

Node.js 的 events 模块允许你实现自己的事件发布/订阅系统,解耦业务逻辑。

2.1 基础用法

const EventEmitter = require('events');

class MyEmitter extends EventEmitter {}

const emitter = new MyEmitter();

emitter.on('data', (msg) => {
  console.log('收到数据:', msg);
});

emitter.emit('data', 'Hello Event!');

2.2 进阶用法:事件参数、一次性事件、移除监听

// 只监听一次
emitter.once('finish', () => console.log('任务完成一次'));

// 移除监听
function onError(err) { console.error('错误:', err); }
emitter.on('error', onError);
emitter.removeListener('error', onError);

2.3 实践场景

  • 日志系统
  • 任务队列
  • 微服务消息总线
  • 多模块解耦通信

3、异步流程设计

异步流程设计是 Node.js 高级开发的难点,常见有三种方式:

3.1 回调方式

嵌套回调,容易形成“回调地狱”,不推荐复杂流程使用。

3.2 事件驱动方式

适合多个步骤、异步事件触发的场景。

const EventEmitter = require('events');
const workflow = new EventEmitter();

workflow.on('step1', () => {
  setTimeout(() => {
    console.log('step1 done');
    workflow.emit('step2');
  }, 500);
});

workflow.on('step2', () => {
  setTimeout(() => {
    console.log('step2 done');
    workflow.emit('finish');
  }, 500);
});

workflow.on('finish', () => {
  console.log('流程结束');
});

workflow.emit('step1');

3.3 Promise/async/await 方式(现代最佳实践)

适合串行和并行异步操作,代码更简洁易维护。

const fs = require('fs/promises');

async function workflow() {
  const a = await fs.readFile('a.txt', 'utf8');
  const b = await fs.readFile('b.txt', 'utf8');
  console.log('a:', a);
  console.log('b:', b);
  // 并行操作
  const [c, d] = await Promise.all([
    fs.readFile('c.txt', 'utf8'),
    fs.readFile('d.txt', 'utf8')
  ]);
  console.log('c:', c, 'd:', d);
}

workflow();

3.4 复杂流程设计建议

  • 串行:用 async/await
  • 并行:用 Promise.all
  • 多步骤/多事件:用事件机制(EventEmitter)
  • 错误处理:统一 try/catch 或事件监听
  • 状态管理:可用状态机模式(如 xstate、machina)

4、总结

  • 事件循环是 Node.js 高性能的基础,理解其阶段有助于优化异步代码。
  • 自定义事件模块(EventEmitter)适合解耦、扩展和多模块通信。
  • 异步流程设计推荐用 async/await + Promise,复杂场景可结合事件机制。

七、事件循环底层原理、异步调度优化、事件总线设计模式

1、事件循环底层原理

1.1 Node.js 事件循环基础

Node.js 的事件循环(Event Loop)是基于 libuv 库实现的。libuv 是一个跨平台的异步 I/O 库,负责管理事件循环、线程池、异步 I/O 等底层细节。

事件循环的核心目标:

  • 让 JavaScript 单线程能高效处理大量并发 I/O 请求。
  • 通过事件队列和回调机制,实现异步非阻塞。

1.2 事件循环的各个阶段

事件循环分为多个阶段,每个阶段处理不同类型的回调:

┌───────────────────────────┐
│   timers (setTimeout等)   │
├───────────────────────────┤
│   pending callbacks       │
├───────────────────────────┤
│   idle, prepare           │
├───────────────────────────┤
│   poll (I/O事件)          │
├───────────────────────────┤
│   check (setImmediate)    │
├───────────────────────────┤
│   close callbacks         │
└───────────────────────────┘
  • timers:处理定时器(setTimeout/setInterval)回调。
  • pending callbacks:处理某些系统操作的回调。
  • idle, prepare:内部使用。
  • poll:处理 I/O 事件,如文件、网络请求。若无事件可处理,可能进入下一个阶段。
  • check:处理 setImmediate 回调。
  • close callbacks:处理如 socket 的关闭事件。

注意:

  • process.nextTick 与 Promise 微任务优先于上述所有阶段执行(属于微任务队列)。

1.3 微任务与宏任务

  • 微任务队列:process.nextTick、Promise.then/catch/finally
  • 宏任务队列:setTimeout、setImmediate、I/O 回调等

Node.js 每执行一个宏任务阶段后,会清空微任务队列。

代码演示

setTimeout(() => console.log('timeout'), 0);
setImmediate(() => console.log('immediate'));
process.nextTick(() => console.log('nextTick'));
Promise.resolve().then(() => console.log('promise'));

console.log('main');

输出顺序:

main
nextTick
promise
timeout
immediate

1.4 底层流程简化

  1. 执行同步代码
  2. 处理微任务队列(nextTick、Promise)
  3. 进入事件循环各阶段,依次处理对应队列的回调
  4. 新的异步任务加入队列,循环继续

2、异步调度优化

2.1 合理选择异步方式

  • I/O 密集型任务:利用 Node.js 的异步 API(如 fs、net、http),避免阻塞主线程
  • CPU 密集型任务:用 worker_threads 或 child_process 多线程/多进程处理

2.2 批量并发控制

一次性启动大量异步任务会造成资源争抢,建议限制并发数:

const pLimit = require('p-limit');
const limit = pLimit(5); // 最多5个并发

const tasks = Array.from({length: 20}, (_, i) =>
  limit(() => fs.promises.readFile(`file${i}.txt`, 'utf8'))
);

Promise.all(tasks).then(results => {
  console.log('所有文件读取完成');
});

2.3 微任务优先级与性能

  • 过量使用 process.nextTick 可能导致主线程阻塞,影响事件循环
  • 适度利用 Promise 微任务进行异步调度,避免回调地狱

2.4 事件驱动解耦

  • 复杂异步流程用事件机制(EventEmitter)管理,避免深层嵌套
  • 利用 once、on、removeListener 等方法灵活调度

2.5 错误处理与健壮性

  • 异步代码统一 try/catch 或事件监听 error
  • 防止未处理的异常导致程序崩溃

3、事件总线设计模式

3.1 事件总线是什么?

事件总线(Event Bus)是一种发布-订阅模式,允许不同模块间通过事件进行解耦通信,常用于大型应用架构。

3.2 Node.js 实现事件总线

可以用 EventEmitter 实现一个简单的事件总线:

// eventBus.js
const EventEmitter = require('events');
class EventBus extends EventEmitter {}
module.exports = new EventBus();

多个模块间通信:

// moduleA.js
const eventBus = require('./eventBus');
eventBus.on('userCreated', user => {
  console.log('新用户:', user);
});

// moduleB.js
const eventBus = require('./eventBus');
eventBus.emit('userCreated', { name: 'Alice', id: 123 });

3.3 进阶:带命名空间的事件总线

class NamespaceEventBus {
  constructor() {
    this.buses = new Map();
  }
  getBus(namespace) {
    if (!this.buses.has(namespace)) {
      this.buses.set(namespace, new (require('events'))());
    }
    return this.buses.get(namespace);
  }
}

const bus = new NamespaceEventBus();
bus.getBus('user').on('created', user => console.log('user created:', user));
bus.getBus('user').emit('created', {name: 'Bob'});

3.4 应用场景

  • 微服务消息通信
  • 前后端模块解耦
  • 任务队列、异步通知
  • 插件/扩展机制

4、总结与实践建议

  • 理解事件循环底层原理有助于编写高性能、健壮的 Node.js 应用
  • 优化异步调度要关注并发量、微任务/宏任务优先级、错误处理
  • 事件总线是大型系统模块间解耦的利器,建议封装统一管理
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

猩火燎猿

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值