Zijue/blog

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 模块,通过手写 onemitoffonce 实现原理彻底搞清楚

实现 on 与 emit 方法

首先新建 events.jsdemo.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 站
*/

上述代码地址:https://github.com/Zijue/ExerciseCodes/tree/master/node_demo/%E6%89%8B%E5%86%99%20events%20%E6%A8%A1%E5%9D%97