六维教程

Node.js events 模块

Node.js http 模块 里服务器每次收到请求都会触发回调,这个机制背后是事件。这一篇解决核心问题,Node.js 到处在说事件,事件驱动到底是什么,以及怎么自己定义事件。events 模块是 Node.js 内置模块,不需要安装。

事件驱动是什么

事件驱动就是程序里有些对象能发事件,其他代码可以提前订阅,事件发生时订阅的代码自动执行。

用生活中的例子理解,食堂打饭窗口。学生是事件源,食堂一响铃就代表开饭事件发生。等着打饭的同学提前排好队,铃一响队伍就开始移动。学生不用一直盯着钟,排队就是订阅,铃声就是触发。

对应到代码里,订阅用 on,触发用 emit

EventEmitter 基本用法

events 模块的核心是 EventEmitter 类,Node.js 大量内置类都继承自它,包括 Node.js fs 模块 的流和 Node.js http 模块 的服务器。

const { EventEmitter } = require('events');

const bell = new EventEmitter();

// on 订阅事件,事件参数可以带数据
bell.on('ring', (who) => {
  console.log(`${who} 去打饭`);
});

// emit 触发事件,后面的参数传给订阅回调
bell.emit('ring', '张三');
bell.emit('ring', '李四');

输出。

张三 去打饭
李四 去打饭

once 只监听一次

const { EventEmitter } = require('events');

const bell = new EventEmitter();

bell.once('ring', () => {
  console.log('只响应一次');
});

bell.emit('ring');
bell.emit('ring');
// 第二次触发没有输出

once 订阅的事件只执行一次,适合一次性通知的场景。

off 移除监听

const { EventEmitter } = require('events');

const bell = new EventEmitter();

function handler() {
  console.log('开饭了');
}

bell.on('ring', handler);
bell.emit('ring');

bell.off('ring', handler);
bell.emit('ring');
// 移除后没有输出

off 移除监听,参数要传之前 on 时的同一个函数,匿名函数没法移除。

为什么 Node.js 选事件驱动

Node.js 是单线程的,如果每个请求都派一个线程处理,线程切换开销巨大。事件驱动配合非阻塞 I/O,让一个线程同时处理成千上万个连接。每次事件触发就是事件循环里的一轮任务,事件循环的详细机制后面单独开一篇讲,这里先记住结论,事件驱动让单线程能高效处理并发。

http 服务器的本质就是这个。

const http = require('http');

const server = http.createServer();

// 等价写法,服务器本身就是 EventEmitter
server.on('request', (req, res) => {
  res.end('Hello');
});

server.listen(3000);

createServer 传回调只是语法糖,本质上服务器在发 request 事件,和 on('request') 完全等价。

自定义事件类

方式一,直接实例化

const { EventEmitter } = require('events');

const myEvent = new EventEmitter();
myEvent.on('x', () => console.log('事件 x'));
myEvent.emit('x');

简单场景够用,但事件和业务逻辑散落在一起,代码一多就乱。

方式二,类继承(推荐)

const { EventEmitter } = require('events');

class User extends EventEmitter {
  login(name) {
    console.log(`${name} 登录`);
    this.emit('login', name);
  }
}

const user = new User();
user.on('login', (name) => {
  console.log(`${name} 的欢迎通知已发送`);
});
user.login('张三');

继承之后,类自己的方法和事件能力都在同一个对象上,事件作为类的一种对外接口,语义清晰,推荐这种写法。

注意事项

error 事件特殊

const { EventEmitter } = require('events');

const e = new EventEmitter();
e.emit('error', new Error('出错了'));
// 没有订阅 error 事件,程序直接抛异常崩溃

error 事件很特殊,emit('error') 时如果没有监听器,Node.js 会直接把错误抛出来,程序崩溃。发 error 事件前,要么保证有人订阅,要么自己 try/catch 兜底。

监听器数量警告

const { EventEmitter } = require('events');

const e = new EventEmitter();

// 默认最多 10 个监听器,超过会警告内存泄漏
e.setMaxListeners(20);
for (let i = 0; i < 20; i++) {
  e.on('x', () => {});
}

同一个事件订阅超过 10 个监听器,Node.js 会提示可能的内存泄漏,可以用 setMaxListeners 调大。真实项目里出现这种情况,通常说明设计有问题,该考虑用数组存数据而不是堆监听器。

实践

创建一个 Logger 类继承 EventEmitter,log 方法按级别触发不同类型事件,外部订阅后按类型打印不同颜色。

// logger.js
const { EventEmitter } = require('events');

class Logger extends EventEmitter {
  log(level, message) {
    const time = new Date().toLocaleTimeString();
    this.emit(level, { time, message });
  }
}

const logger = new Logger();

logger.on('info', ({ time, message }) => {
  console.log(`\x1b[32m[${time}] INFO ${message}\x1b[0m`);
});

logger.on('error', ({ time, message }) => {
  console.log(`\x1b[31m[${time}] ERROR ${message}\x1b[0m`);
});

logger.log('info', '服务启动成功');
logger.log('info', '收到新请求');
logger.log('error', '数据库连接失败');

运行。

node logger.js

\x1b[32m 是终端颜色转义序列,绿色,\x1b[31m 红色,\x1b[0m 恢复默认。info 事件打印绿色日志,error 事件打印红色日志,时间和内容通过事件参数传递。

事件驱动讲完后,还差一块拼图,那就是二进制数据 Buffer。fs 和 http 里到处都在传字节流,Node.js buffer 模块 专门讲它。

上一篇
Node.js http 模块
下一篇
Node.js buffer 模块