Nodejs中有一个Events模块,它实现并且实践了基于消息事件的程序运行模式。在nodejs中,大量使用这个模型表示和处理各种各样的操作,如TCP连接建立、文件打开、数据包发送等等。事实上,Events是Node.js实现高扩展性和高性能的关键,许多Node.js核心API都是基于这个模型来构造的,所以它是一个非常重要的基础编程模型。笔者认为,作为一个合格的nodejs应用开发者,必须对它有一定程度的理解和掌握。
观察者模式
基于消息事件的发送和处理,是一种通用和类型化的编程模型和范式。它和常规的使用参数进行的直接方法调用不同,它分为事件消息的发送和接收两个方面,通过消息通知订阅的方式关联起来。
但如果我们将这个机制拆解开来,我们将会发现,它实际上就是一个观察者模式的实现,并且在上面扩展和叠加了消息发送和接收的形式。所以,先来了解观察者模式的基本原理和实现方法,非常有助于我们理解这个编程模型。
观察者模式(Observer Pattern)是一种通用和抽象的行为设计模式,通常用于表示和实现动态的"一对多"这样一种实体对象之间的依赖和互动关系。这里的关键字是"动态",也就是说,这种关系的建立和解除,是可以按照需求,进行动态的操作的,这样就给程序架构,带来了非常大的灵活性。
观察者模式的基本架构如图所示:
基本而言,整个观察者模式由主题(Subject)和观察者(Observer)两个类型角色构成。
主题中的要素包括:
- 观察者列表
- 注册方法
- 取消注册方法
- 通知方法
主题对象是观察者模式的核心,它负责维护一组观察者对象,可以就是一个简单的对象列表,并在状态发生变化时通知它们。所以,它需要提供注册和取消注册方法,可以让观察者实体加入和退出观察者列表;当需要进行通知时,主题可以调用通知方法,这个方法会遍历观察者列表,依次调用它们提供的标准更新接口方法,来实现通知的动作。
观察者的要素包括:
- 更新(通知接收)方法
观察者对象就比较简单了,它只需要根据标准和规范,并结合业务需求,提供和实现标准的更新方法接口就可以了。然后,它会作为一个实例,被添加到主题的观察者列表当中。当通知发生时,会这个更新接口方法,会被调用和执行,从而完成业务操作。使用面向对象的编程机制,在主题中的观察者不需要是相同的类,只需要它们的类或者实例,实现了更新方法接口就可以了,这样就可以完全解耦主题和观察者之间的类依赖关系。
理解了这个基本架构和原理之后,我们后面就会比较容易理解和看到,在nodejs中的Events模块,是如何实现和工作的。
Nodejs Events模块
让我们回到nodejs,来看看这个Events模块。
events模块的核心是EventEmitter(译为事件发射器)对象。其实笔者不是特别理解它的命名方式。Emitter是发射的意思,但实际上,我们在应用的时候,是没有区分发射和接收事件的,都是使用这个对象来进行处理的,这个我们会在后面的实例代码中可以看到。
EventEmitter对象的主要属性和方法如下。
- on/addListener: 注册事件触发和触发时的回调方法
- once: 仅在第一次触发时,只执行一次的回调方法
- emit: 执行事件触发
- off/removeListener: 关闭移除侦听事件
- prependListener/prependOnceListener: 注册优先的侦听者
- removeAllListeners:移除所有侦听
下面,我们通过讨论的一般工作流程,来了解和熟悉一下这个对象的使用方式。
一般工作流程
使用events模块和类的一般流程和生命周期如下:
1 引入Events模块 在使用events相关特性之前,需要先引入node:events模块。默认的引入的结果是一个EventEmitter类。
2 创建事件发射器实例
可以使用EventEimtter类,创建一个新的事件发射器实例。然后在这个实例上开展相关的工作和操作。
3 定义实现事件回调函数
在注册了消息事件之后,需要编写对应的事件触发时需要的业务操作,形式是一个回调函数。函数的参数就是消息发射调用时使用的参数。
4 为发射器创建和绑定侦听事件
发射器实例创建之后,就可以为其绑定消息事件了。 一般使用on()方法。这个方法接收两个参数。第一个参数是可自定义的事件名称,第二个参数是相应的回调函数。用于在接收到同名的消息时,触发并且执行。
如果只希望这个事件执行一次,可以使用once()方法。其他用法和on一样。
5 执行事件发射
可以调用发射器实例的emit方法,来发射一个事件。这个方法支持一个以上的可变参数。第一个参数是事件名称;其他参数是附加数据,这些数据会作为参数,在消息侦听事件的触发时,作为回调方法的参数注入。
6 移除侦听事件
按照以上要点,笔者编写了相关的测试示例程序,帮助读者直观清晰的体会这个流程和操作方式。
e.js
// 引用 Event类
const Event = require("node:events");
// 创建Event实例
const ebus = new Event();
// 消息事件回调方法
function pong(data) {
console.log('Pong', data);
};
// 事件和侦听定义
ebus.once('ping', ()=>console.log("Pong Once!") );
ebus.on('ping', pong);
// 消息事件发射
ebus.emit('ping',1);
ebus.emit('ping',2);
// 解除消息绑定
ebus.removeListener('ping');
// 无效事件,因为已经解除了绑定
ebus.emit('ping',3);
按照观察者模式,我们其实可以看到,这个EventEmitter实际上就是一个主题,但是它稍做了一下改进。这里没有明确的观察者对象,它在这里是使用回调方法来实现的,或者可以说这个回调方法(JS中,函数是可以作为变量的),就是观察者和通知方法。这个on和once方法,其实就是在进行观察者的注册,观察者的列表,就是一系列回调方法,当进行对应事件的发射时,就是在触发主题的观察者通知操作,它会遍历所有符合条件的观察者(订阅了对应事件的),然后直接调用这些回调方法,来实现事件触发。
明确了原理和流程,我们来看看在nodejs中,是如何应用的。
在nodejs中的一些应用
Events模块和类,在nodejs中,其实是有非常广泛的应用的,是一个重要的基础技术。包括:
- Stream模块
在nodejs的技术文档,Stream章节中,有这么一句描述:
Streams can be readable, writable, or both. All streams are instances of EventEmitter.
流可以读,或者写,也可以同时读写。所有的流都是EventEmitter的实例
由于Stream是Nodejs中的一个基础类,构建在它上面的相关类和应用,自然也会基于Events。
- FS模块
fs模块中相关的流数据处理,本质上是stream的一个扩展应用,在这里我们可以更清楚的看到消息事件驱动编程的方式:
js
const fs = require('node:fs');
const rr = fs.createReadStream('foo.txt');
rr.on('readable', () => {
console.log(`readable: ${rr.read()}`);
});
rr.on('end', () => {
console.log('end');
});
- Net
net是nodejs的基本网络模块,它实际上是对TCP协议的实现。其中最重要的类是:net.Socket,它也是stream.Duplex的子类。这个类是一个TCP socket或者IPC端点(Windows下的命名管道,Unix下的Sockets)的抽象,同样也是一个EventEmitter实例。
下面的例子是一个TCP服务的实现,都是消息事件模式:
js
const net = require('node:net');
const server = net.createServer((c) => {
// 'connection' listener.
console.log('client connected');
c.on('end', () => {
console.log('client disconnected');
});
c.write('hello\r\n');
c.pipe(c);
});
server.on('error', (err) => {
throw err;
});
server.listen(8124, () => {
console.log('server bound');
});
- HTTP模块
HTTP模块基于Net,也大量的使用到了数据流,当然也会基于EventsEmitter。无论是在客户端还是服务端,我们都可以看到这些应用方式:
js
// http 请求
const http = require('node:http');
const options = {
host: 'www.google.com',
};
const req = http.get(options);
req.end();
req.once('response', (res) => {
const ip = req.socket.localAddress;
const port = req.socket.localPort;
console.log(`Your IP address is ${ip} and your source port is ${port}.`);
// Consume response object
});
// http 服务升级WebSocket
const server = http.createServer((req, res) => {
res.writeHead(200, { 'Content-Type': 'text/plain' });
res.end('okay');
});
server.on('upgrade', (req, socket, head) => {
socket.write('HTTP/1.1 101 Web Socket Protocol Handshake\r\n' +
'Upgrade: WebSocket\r\n' +
'Connection: Upgrade\r\n' +
'\r\n');
socket.pipe(socket); // echo back
});
- Process
Process也是EventEmitter的实例,包括Worker也是它的子类。因为这些都需要使用消息事件的方式,进行相关的状态管理和数据传输。如下面的代码示例:
js
const process = require('node:process');
process.on('beforeExit', (code) => {
console.log('Process beforeExit event with code: ', code);
});
process.on('exit', (code) => {
console.log('Process exit event with code: ', code);
});
console.log('This message is displayed first.');
这样的例子还有很多,这里不再累述,只是需要表明,Events是nodejs的一项重要和基础的技术,值得我们深入理解和掌握。
示例:项目消息总线
笔者在某应用系统中,使用Events实现了消息总线,用于传递数据和状态变化等信息。也算是一个Events应用的一种使用场景和实例了,这里分享一下。
首先定义了一个工具模块如下:
ebus.js
const Event = require("node:events");
// some ev constant
const
EV_MESSAGE = "message",
EV_DATA = "data";
// 使用闭包和立即执行函数创建单例
const Singleton = (function() {
let instance;
// 在这里创建单例的实例
const createInstance = ()=> new Event();
return {
getInstance: function() {
if (!instance) instance = createInstance(); // 如果实例不存在,则创建一个新实例
return instance;
}
};
})();
module.exports = { EV_MESSAGE, EV_DATA,
send: (data, ev = EV_MESSAGE)=> Singleton.getInstance().emit(ev, data),
subscript:(cb, ev = EV_MESSAGE)=>Singleton.getInstance().on(ev, cb)
};
简单解释和说明一下:
- 本工具类,使用一个Events实例的单例,来作为消息总线本体
- 定义了一些常数消息名称,可以复用和共享,不同的名称,可以看成不同的总线通道
- 封装了一个send方法,用于向总线发送消息,可以使用默认总线或者自定义总线
- 封装了一个subscript方法,用于订阅特定的总线通道,默认为"message"
然后是如何使用这个工具模块:
js
const ebus = require("./ebus");
// 订阅通知,业务操作
ebus.subscript(data=>{
console.log("From Bus:", data);
});
// 发送数据,应该在另一个模块或者逻辑中
ebus.send("Hello "+ Date.now());
setTimeout(()=>{
ebus.send("Hello "+ Date.now());
},1500);
逻辑上而言,如果在一个比较大的nodejs应用项目中,可以在整个项目范围内,共享使用这个工具类模块来进行消息的传输和处理。
小结
本文探讨了nodejs的Events模块,涉及其基础理论和基本原理,一般的应用和工作流程。然后讨论了其在nodejs系统中的一些应用,以及在业务系统中的一个消息总线的应用示例。