6.events 模块
Opened this issue · 0 comments
Zijue commented
events 模块介绍
events 模块是 nodejs 中一个很重要的模块,可以称之为发布/订阅模式。解决了多状态异步操作的响应问题
官方示例
const EventEmitter = require('events');
class MyEmitter extends EventEmitter {}
const myEmitter = new MyEmitter();
myEmitter.on('event', () => {
console.log('触发事件-1');
});
myEmitter.on('event', () => {
console.log('触发事件-2');
});
myEmitter.emit('event');
手写 events 模块核心方法
为了更好的理解 events 模块,通过手写
on
、emit
、off
、once
实现原理彻底搞清楚
实现 on 与 emit 方法
首先新建 events.js
、demo.js
文件
// events.js
function EventEmitter() {
this._events = {}; // 实例属性
}
// {'绑定事件': [fn1, fn2, fn3]}
EventEmitter.prototype.on = function (eventName, callback) {
if (!this._events) this._events = {}; // 原型继承不会继承实例属性;检查子类实例是否有 _events 属性,没有新建
if (!this._events[eventName]) {
this._events[eventName] = [callback];
} else {
this._events[eventName].push(callback);
}
}
EventEmitter.prototype.emit = function (eventName, ...args) {
if (!this._events) this._events = {}; // 原型继承不会继承实例属性;检查子类实例是否有 _events 属性,没有新建
if (this._events[eventName]) {
this._events[eventName].forEach(fn => fn(...args));
}
}
module.exports = EventEmitter;
// demo.js
const EventEmitter = require('./events'); // 自定义 events 模块,实现源码核心功能
const util = require('util');
function XiaoChi(){}
util.inherits(XiaoChi, EventEmitter);
let xiaochi = new XiaoChi();
xiaochi.on('小池的日常', (...args)=>{
console.log('吃饭', ...args);
})
xiaochi.on('小池的日常', (...args)=>{
console.log('睡觉', ...args);
})
xiaochi.on('小池的日常', (...args)=>{
console.log('撸代码', ...args);
})
xiaochi.emit('小池的日常', '偶尔耍耍 b 站');
on 方法主要就是将注册的事件添加到绑定事件的数组中,触发 emit 方法则是遍历执行相应事件的数组里方法。需要注意的是原型继承不会继承实例属性
上述方法中使用 util. 方法实现原型继承,顺便扩展一下 nodejs 中原型继承的几种方法
// node 原型继承的几种方式
XiaoChi.prototype.__proto__ = EventEmitter.prototype;
XiaoChi.prototype = Object.create(EventEmitter.prototype); // ES5 提供的方法
util.inherits(XiaoChi, EventEmitter); // node 新版本提供
util.inherits 实现原理是 Object.setPrototypeOf(XiaoChi.prototype, EventEmitter.prototype); // ES6 提供的方法
class XiaoChi extends EventEmitter {}; // ES6 语法,nodejs 源码暂时使用的是 ES5 语法
实现 off 与 once 方法
// events.js
...
EventEmitter.prototype.off = function (eventName, callback) {
if (!this._events) this._events = {}; // 原型继承不会继承实例属性;检查子类实例是否有 _events 属性,没有新建
if (this._events[eventName]) {
this._events[eventName] = this._events[eventName].filter(fn => fn !== callback); // 过滤掉要移除的回调函数
}
}
...
// demo.js
...
xiaochi.on('小池的日常', (...args)=>{
console.log('吃饭', ...args);
})
xiaochi.on('小池的日常', (...args)=>{
console.log('睡觉', ...args);
})
xiaochi.on('小池的日常', (...args)=>{
console.log('撸代码', ...args);
})
playGame = (...args)=>{
console.log('打游戏', ...args);
}
xiaochi.on('小池的日常', playGame)
xiaochi.emit('小池的日常', '偶尔耍耍 b 站');
xiaochi.off('小池的日常', playGame); // 取消绑定事件的回调函数必须有名,匿名函数不可以取消
xiaochi.emit('小池的日常', '偶尔耍耍 b 站');
/* 输出的结果
吃饭 偶尔耍耍 b 站
睡觉 偶尔耍耍 b 站
撸代码 偶尔耍耍 b 站
打游戏 偶尔耍耍 b 站
吃饭 偶尔耍耍 b 站
睡觉 偶尔耍耍 b 站
撸代码 偶尔耍耍 b 站
*/
以上为 off 方法的实现原理
once 方法则是执行一次绑定事件的回调函数后,通过 off 方法从绑定事件回调函数数组中移除。实现原理如下:
// events.js
...
EventEmitter.prototype.once = function (eventName, callback) {
// 通过 AOP 编程的方式实现先执行 on 方法再执行 off 方法
const _once = (...args) => {
callback(...args);
this.off(eventName, _once);
}
this.on(eventName, _once);
}
...
// demo.js
...
xiaochi.on('小池的日常', (...args) => {
console.log('吃饭', ...args);
})
xiaochi.on('小池的日常', (...args) => {
console.log('睡觉', ...args);
})
xiaochi.on('小池的日常', (...args) => {
console.log('撸代码', ...args);
})
playGame = (...args) => {
console.log('打游戏', ...args);
}
xiaochi.once('小池的日常', playGame)
xiaochi.emit('小池的日常', '偶尔耍耍 b 站');
xiaochi.emit('小池的日常', '偶尔耍耍 b 站');
/* 输出的结果
吃饭 偶尔耍耍 b 站
睡觉 偶尔耍耍 b 站
撸代码 偶尔耍耍 b 站
打游戏 偶尔耍耍 b 站
吃饭 偶尔耍耍 b 站
睡觉 偶尔耍耍 b 站
撸代码 偶尔耍耍 b 站
*/
上述实现的 once 代码还有一点问题,通过代码来演示;在执行 emit 之前,执行 off 方法取消 once 的事件绑定
// demo.js
...
xiaochi.on('小池的日常', (...args) => {
console.log('吃饭', ...args);
})
xiaochi.on('小池的日常', (...args) => {
console.log('睡觉', ...args);
})
xiaochi.on('小池的日常', (...args) => {
console.log('撸代码', ...args);
})
playGame = (...args) => {
console.log('打游戏', ...args);
}
xiaochi.once('小池的日常', playGame)
xiaochi.off('小池的日常', playGame); // 在执行 emit 前取消 once 绑定的回调函数
xiaochi.emit('小池的日常', '偶尔耍耍 b 站');
xiaochi.emit('小池的日常', '偶尔耍耍 b 站');
/* 输出的结果
吃饭 偶尔耍耍 b 站
睡觉 偶尔耍耍 b 站
撸代码 偶尔耍耍 b 站
打游戏 偶尔耍耍 b 站
吃饭 偶尔耍耍 b 站
睡觉 偶尔耍耍 b 站
撸代码 偶尔耍耍 b 站
*/
从结果看,off 方法并没有生效,这是因为 once 绑定事件的回调函数外层套了一层函数,所以直接取消无法匹配。解决方法如下
// events.js
...
EventEmitter.prototype.off = function (eventName, callback) {
if (!this._events) this._events = {}; // 原型继承不会继承实例属性;检查子类实例是否有 _events 属性,没有新建
if (this._events[eventName]) {
this._events[eventName] = this._events[eventName].filter(fn => fn !== callback && fn.listener !== callback); // 过滤掉要移除的回调函数
}
}
EventEmitter.prototype.once = function (eventName, callback) {
// 通过 AOP 编程的方式实现先执行 on 方法再执行 off 方法
const _once = (...args) => {
callback(...args);
this.off(eventName, _once);
}
_once.listener = callback; // 在 _once 上添加一个监听属性指向 callback 函数
this.on(eventName, _once);
}
...
// 再次执行 demo.js,发现 off 方法作用生效了
/* 输出的结果
吃饭 偶尔耍耍 b 站
睡觉 偶尔耍耍 b 站
撸代码 偶尔耍耍 b 站
吃饭 偶尔耍耍 b 站
睡觉 偶尔耍耍 b 站
撸代码 偶尔耍耍 b 站
*/
newListener 绑定事件原理
newListener 是 nodejs 内部实现的一个绑定事件,在使用 on 方法绑定其它事件时触发该绑定事件的回调函数
实现原理如下
// events.js
...
// {'绑定事件': [fn1, fn2, fn3]}
EventEmitter.prototype.on = function (eventName, callback) {
if (!this._events) this._events = {}; // 原型继承不会继承实例属性;检查子类实例是否有 _events 属性,没有新建
// newListener 实现原理
if (eventName !== 'newListener'){ // 源码中实现就是当绑定事件时,先触发 newListener 事件的回调再将绑定事件添加到对应的数组中
this.emit('newListener', eventName);
}
if (!this._events[eventName]) {
this._events[eventName] = [callback];
} else {
this._events[eventName].push(callback);
}
}
...
// demo.js
...
let xiaochi = new XiaoChi();
xiaochi.on('newListener', (eventName) => { // 每次绑定事件都会触发此函数(先出发 newListener 事件,再将绑定事件添加在回调数组中)
// 只要绑定事件我就立即触发
process.nextTick(() => { // 使用 process.nextTick() 异步方式实现事件绑定添加发生在触发之前(源码是 newListener 事件触发先于事件的绑定)
xiaochi.emit(eventName, '偶尔逛逛 b 站');
})
});
xiaochi.on('小池的日常', (...args) => {
console.log('吃饭', ...args);
})
xiaochi.on('小池的日常', (...args) => {
console.log('睡觉', ...args);
})
xiaochi.on('小池的日常', (...args) => {
console.log('撸代码', ...args);
})
...
/* 输出的结果
吃饭 偶尔逛逛 b 站
睡觉 偶尔逛逛 b 站
撸代码 偶尔逛逛 b 站
吃饭 偶尔逛逛 b 站
睡觉 偶尔逛逛 b 站
撸代码 偶尔逛逛 b 站
吃饭 偶尔逛逛 b 站
睡觉 偶尔逛逛 b 站
撸代码 偶尔逛逛 b 站
*/
上面代码绑定事件都执行了三次(源码如此),如果想只执行一次,可以自己添加防抖代码
// demo.js
...
let pending = false;
let xiaochi = new XiaoChi();
xiaochi.on('newListener', (eventName) => { // 每次绑定事件都会触发此函数(先出发 newListener 事件,再将绑定事件添加在回调数组中)
if (pending) return;
pending = true;
// 只要绑定事件我就立即触发
process.nextTick(() => { // 使用 process.nextTick() 异步方式实现事件绑定添加发生在触发之前(源码是 newListener 事件触发先于事件的绑定)
xiaochi.emit(eventName, '偶尔逛逛 b 站');
pending = false;
})
});
xiaochi.on('小池的日常', (...args) => {
console.log('吃饭', ...args);
})
xiaochi.on('小池的日常', (...args) => {
console.log('睡觉', ...args);
})
xiaochi.on('小池的日常', (...args) => {
console.log('撸代码', ...args);
})
...
/* 输出的结果
吃饭 偶尔逛逛 b 站
睡觉 偶尔逛逛 b 站
撸代码 偶尔逛逛 b 站
*/