Nodejs开发进阶F-Event

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系统中的一些应用,以及在业务系统中的一个消息总线的应用示例。

相关推荐
翔云API1 分钟前
人证合一接口:智能化身份认证的最佳选择
大数据·开发语言·node.js·ocr·php
-seventy-8 分钟前
对 JavaScript 原型的理解
javascript·原型
白总Server32 分钟前
MongoDB解说
开发语言·数据库·后端·mongodb·golang·rust·php
谢尔登33 分钟前
Babel
前端·react.js·node.js
计算机学姐44 分钟前
基于python+django+vue的家居全屋定制系统
开发语言·vue.js·后端·python·django·numpy·web3.py
lxcw1 小时前
npm ERR! code CERT_HAS_EXPIRED npm ERR! errno CERT_HAS_EXPIRED
前端·npm·node.js
秋沐1 小时前
vue中的slot插槽,彻底搞懂及使用
前端·javascript·vue.js
QGC二次开发1 小时前
Vue3 : Pinia的性质与作用
前端·javascript·vue.js·typescript·前端框架·vue
布丁椰奶冻1 小时前
解决使用nvm管理node版本时提示npm下载失败的问题
前端·npm·node.js
程序员-珍2 小时前
SpringBoot v2.6.13 整合 swagger
java·spring boot·后端