Node 入门(7):events 模块和发布订阅模式

简介: 本文会介绍 events 模块的主要作用和使用方式,以及自己实现一个简单的发布订阅模式,帮助加深理解。

前言

本文会介绍 events 模块的主要作用和使用方式,以及自己实现一个简单的发布订阅模式,帮助加深理解。

events 模块

文档地址,点击访问

Node.js 是基于事件驱动实现的异步操作。 事件驱动依赖就是的 events 模块。

events 模块导出一个 EventEmitter 类,它是发布订阅模式的一种实现。

无论你是了解发布订阅模式,只要你有过前端开发的经验,那么对于 DOM事件监听,事件解绑等操作一定不陌生。其实这就是一种发布订阅。

平时开发中我们很少会直接使用这个模块。这是因为 Node.js 的很多内置模块,比如 fs , http 等模块,都是继承自 EventEmitter 类而实现的。

也就是说,我们平时经常用到的模块,就已经具备了发布订阅(事件监听、事件触发)的能力。后面会介绍到 Stream 和 http 模块的使用,到时候就会发现,到处都有发布订阅的影子。

EventEmitter 类

events 模块对外暴露 EventEmitter 类。

打印一下它的原型对象:

const EventEmitter = require('events')

console.log(EventEmitter.prototype)

有很多方法,光看名字我们就能知道每个方法的作用是什么:

  • on:订阅事件
  • emit:发布事件
  • once:只订阅一次,也就是只能触发一次
  • removeListener:移除订阅者
  • removerAllListeners:移除所有的订阅者
  • off:removeLister 方法的别名

使用

EventEmitter 类的使用方式有两种。

首先它是一个构造函数(源码中以构造函数实现),所以可以通过实例化的方法来使用。

其次,node 的很多模块都是继承自它,所以子类除了自身的能力之外,也具备了发布订阅的能力。

这里演示下第二种。

const EventEmitter = require('events')


class Reader extends EventEmitter {
  // ... 
}

const reader = new Reader()

const buy = bookName => {
  console.log('买新书:', bookName)
}

// 订阅小册上新的事件;当有上新后,就买买买
reader.on('new', buy)

// 小册上新了
reader.emit('new', '《TypeScript 全面进阶指南》') // 买新书: 《TypeScript 全面进阶指南》
// 小册又上新了
reader.emit('new', '《NestJS 项目实战》') // 买新书: 《《NestJS 项目实战》》

// 最近不打算买书了: 取消订阅
reader.off('new', buy)

// 小册上新了。但是取消了订阅,所以消息不会发给你了
reader.emit('new', 'React 上天入地》') // 没有效果

实现发布订阅

实现发布订阅,也是常考的一道面试题。

实现起来也并不复杂,结合注释很方便理解。

// 定义一个构造函数
function EventEmitter() {
  //  存储订阅者,也就是事件处理函数的容器。
  // 数据结构为:{ event_type1: [handler1, ...], ... }
  this._events = Object.create(null) // 
}

// 订阅事件的方法
EventEmitter.prototype.on = function on(type, handler) {
  // 首先判断要注册的事件对应的容器是否存在,若存在则将新的 handler 存入,否则先创建一个对应的容器
  let events = this._events[type]
  if (!events) {
    events = this._events[type] = []
  }

  events.push(handler)
}

// 只订阅一次事件的方法
// 思路:利用切片编程的思想,给原始 handler 包装一层。
// 正常的执行逻辑:handler 执行
// 切片编程的逻辑:handler 执行,执行完再调用一次 off 。相当于扩展了 handler 方法
EventEmitter.prototype.once = function on(type, handler) {
  const fn = (...args) => {
    handler(...args)
    this.off(type, fn)
  }

  this.on(type, fn)
}

// 发布事件的方法
EventEmitter.prototype.emit = function emit(type, ...args) {
  // 从事件容器中取出对应的 handler 依次去执行
  // 若发布的事件不存在,则不进行处理
  let events = this._events[type]
  // events可能存在空数组的情况,需要处理
  if (!events || events.length === 0) {
    return
  }
  events.forEach(handler => handler(...args))
}


// 移除事件监听
EventEmitter.prototype.off = function off(type, handler) {
  let events = this._events[type]
  if (!events || events.length === 0) {
    return
  }

  // 过滤掉容器中需要解除监听的方法
  this._events[type] = events.filter(item => handler !== item)
}

// 测试
const emitter = new EventEmitter()

const play = (...args) => {
  console.log('play', ...args)
}

const sing = (...args) => {
  console.log('sing', ...args)
}

emitter.on('play', play)
emitter.on('sing', sing)


emitter.emit('sing', 'rap')  // sing rap
emitter.emit('play', '篮球')  // play 篮球

emitter.off('play', play)
emitter.emit('play')


const dance = () => {
  console.log('dance')
}

emitter.once('dance', dance)

emitter.emit('dance') // dance
emitter.emit('dance') // 已移除订阅,不执行

延申阅读:eventbus 和 mitt

有一道经典的面试题:Vue 组件通信的方式有哪些?

除了常见的通过属性,自定义事件,借助状态库,在 Vue2 中还有一种方式,叫作 eventBus 事件总线。

EventBus 本质上就是一个发布订阅模式。但是在 Vue3 中被移除了,官方推荐使用一个第三方库 mitt 来做发布订阅。

Mitt是一个十分小巧的发布订阅库,大约只有200字节左右。而且用法上也没有什么太大的差异,稍微看一下文档就会用了。

import mitt from 'mitt'

const emitter = mitt()

// 订阅事件
emitter.on('foo', e => console.log('foo', e) )

// 订阅所有的事件
emitter.on('*', (type, e) => console.log(type, e) )

// 触发事件
emitter.emit('foo', { a: 'b' })

// 清除所有的订阅者
emitter.all.clear()

// 使用事件处理函数的引用,方便移除监听 
function onFoo() {}
emitter.on('foo', onFoo)   // listen
emitter.off('foo', onFoo)  // unlisten

总结

本文介绍了 Node.js 中 events 模块的使用,它主要导出一个 EventEmitter 类来做发布订阅。Node.js 的很多核心模块都继承自 EventEmitter 类。

它是一个底层的模块,但通常很少直接使用它。等后面讲到 Stream,再后面讲到 http 模块的时候,就能体会到它的存在了。

目录
相关文章
|
4月前
|
分布式计算 JavaScript 前端开发
超级实用!详解Node.js中的lodash模块和async模块
超级实用!详解Node.js中的lodash模块和async模块
|
4月前
|
JSON JavaScript 前端开发
超级实用!详解Node.js中的util模块和express模块
超级实用!详解Node.js中的util模块和express模块
|
4月前
|
JavaScript
超级实用!详解Node.js中的path模块和events模块
超级实用!详解Node.js中的path模块和events模块
|
3月前
|
JavaScript
Node.js【GET/POST请求、http模块、路由、创建客户端、作为中间层、文件系统模块】(二)-全面详解(学习总结---从入门到深化)
Node.js【GET/POST请求、http模块、路由、创建客户端、作为中间层、文件系统模块】(二)-全面详解(学习总结---从入门到深化)
30 0
|
3月前
|
消息中间件 Web App开发 JavaScript
Node.js【简介、安装、运行 Node.js 脚本、事件循环、ES6 作业队列、Buffer(缓冲区)、Stream(流)】(一)-全面详解(学习总结---从入门到深化)
Node.js【简介、安装、运行 Node.js 脚本、事件循环、ES6 作业队列、Buffer(缓冲区)、Stream(流)】(一)-全面详解(学习总结---从入门到深化)
81 0
|
11天前
|
消息中间件 监控 JavaScript
Node.js中的进程管理:child_process模块与进程管理
【4月更文挑战第30天】Node.js的`child_process`模块用于创建子进程,支持执行系统命令、运行脚本和进程间通信。主要方法包括:`exec`(执行命令,适合简单任务)、`execFile`(安全执行文件)、`spawn`(实时通信,处理大量数据)和`fork`(创建Node.js子进程,支持IPC)。有效的进程管理策略涉及限制并发进程、处理错误和退出事件、使用流通信、谨慎使用IPC以及监控和日志记录,以确保应用的稳定性和性能。
|
12天前
|
缓存 JavaScript 前端开发
Node.js的模块系统:CommonJS模块系统的使用
【4月更文挑战第29天】Node.js采用CommonJS作为模块系统,每个文件视为独立模块,通过`module.exports`导出和`require`引入实现依赖。模块有独立作用域,保证封装性,防止命名冲突。引入的模块会被缓存,提高加载效率并确保一致性。利用CommonJS,开发者能编写更模块化、可维护的代码。
|
26天前
|
JavaScript API
node.js之模块系统
node.js之模块系统
|
29天前
|
Web App开发 JavaScript 前端开发
【Node系列】node核心模块util
Node.js的核心模块util为开发者提供了一些常用的实用工具函数。这些函数能够很方便地进行对象的继承、类型判断以及其他工具函数的实现。
22 2
|
29天前
|
域名解析 网络协议 JavaScript
【Node系列】node工具模块
Node.js有多个内置的工具模块,这些模块提供了用于执行各种任务的功能。
24 2