【node不完全指西】EventEmitter (事件发布/订阅模式)解析

16

从node异步编程解决方案说起吧:

  • 事件发布/订阅模式
  • Promise/deferred模式
  • 流程控制库

事件发布/订阅模式

事件监听器模式是一种广泛运用于异步编程的模式,是回调函数的事件话,又称发布/订阅模式。

主要实现的几个功能包括

  • on
  • remove
  • once
  • emit

废话少说,我们来简单的实现一个事件监听函数吧

首先创建一个eventEmitter函数
function EventEmitter() {
    // 用Object.create(null)代替空对象{}
    // 好处是无杂质,不继承原型链
    // _events来保存观察着队列的信息
    this._events = Object.create(null);
}

因为过多的侦听器占用大量内存,导致内存泄漏,所以侦听器的个数一般不会超过10个,否则会有warnning警告⚠️
接下来是一些默认的设置

// 默认最多的绑定次数
EventEmitter.defaultMaxListeners = 10;
// 同on方法
EventEmitter.prototype.addListener = EventEmitter.prototype.on;
// 返回监听的事件名
EventEmitter.prototype.eventNames = function () {
    return Object.keys(this._events);
};
// 设置最大监听数
EventEmitter.prototype.setMaxListeners = function (n) {
    this._count = n;
};
// 返回监听数
EventEmitter.prototype.getMaxListeners = function () {
    return this._count ? this._count : this.defaultMaxListeners;
};
接下来是on函数的实现
EventEmitter.prototype.on = function (type, cb, flag) {
    // 不是newListener 就应该让newListener执行以下
    if (type !== 'newListener') {
        this._events['newListener'] && this._events['newListener'].forEach(listener => {
            listener(type);
        });
    }
    if (this._events[type]) {
        // 根据传入的flag来决定是向前还是向后添加
        if (flag) {
            this._events[type].unshift(cb);
        } else {
            this._events[type].push(cb);
        }
    } else {
        this._events[type] = [cb];
    }
    // 监听的事件不能超过了设置的最大监听数
    if (this._events[type].length === this.getMaxListeners()) {
        console.warn('警告-监听器Number过大');
    }
};

解析:
on函数是帮定的初始函数,首先判断是否是首次进行侦听,如果是的话,先进行一遍初始化函数
接下来在——events队列里找到指针为type的地方,根据flag判断是在队列尾还是头加入callback函数

接下来是once监听一次的实现方法
// 监听一次
EventEmitter.prototype.once = function (type, cb, flag) {
    // 先绑定,调用后删除
    function wrap() {
        cb(...arguments);
        this.removeListener(type, wrap);
    }
    // 自定义属性
    wrap.listen = cb;
    this.on(type, wrap, flag);
};

解析:
实现为在callback上包装一层remove操作,再当做一个新的callback传入on函数
这样的的话在首次执行回调的时候就会执行remove操作,达到执行一次就删除的操作

接下来是remove函数,删除一个type的侦听器
EventEmitter.prototype.removeListener = function (type, cb) {
    if (this._events[type]) {
        this._events[type] = this._events[type].filter(listener => {
            return cb !== listener && cb !== listener.listen;
        });
    }
};

解析:
传入type和要删除的callback,对type标记的数组进行 filter操作,假如cb cb === listener则过滤掉

删除所有
EventEmitter.prototype.removeAllListener = function () {
    this._events = Object.create(null);
};
接下来是发布函数 emit
EventEmitter.prototype.emit = function (type, ...args) {
    if (this._events[type]) {
        this._events[type].forEach(listener => {
            listener.call(this, ...args);
        });
    }
};

解析:
也比较直观,如果events里面存在type的监听器队列,则队列里的每个回调都执行一遍,并且用call函数绑定this和arg

完整代码

//EventEmitter.js


function EventEmitter() {
    // 用Object.create(null)代替空对象{}
    // 好处是无杂质,不继承原型链的东东
    this._events = Object.create(null);
}
// 默认最多的绑定次数
EventEmitter.defaultMaxListeners = 10;
// 同on方法
EventEmitter.prototype.addListener = EventEmitter.prototype.on;
// 返回监听的事件名
EventEmitter.prototype.eventNames = function () {
    return Object.keys(this._events);
};
// 设置最大监听数
EventEmitter.prototype.setMaxListeners = function (n) {
    this._count = n;
};
// 返回监听数
EventEmitter.prototype.getMaxListeners = function () {
    return this._count ? this._count : this.defaultMaxListeners;
};
// 监听
EventEmitter.prototype.on = function (type, cb, flag) {
    // 默认值,如果没有_events的话,就给它创建一个
    if (!this._events) {
        this._events = Object.create(null);
    }
    // 不是newListener 就应该让newListener执行以下
    if (type !== 'newListener') {
        this._events['newListener'] && this._events['newListener'].forEach(listener => {
            listener(type);
        });
    }
    if (this._events[type]) {
        // 根据传入的flag来决定是向前还是向后添加
        if (flag) {
            this._events[type].unshift(cb);
        } else {
            this._events[type].push(cb);
        }
    } else {
        this._events[type] = [cb];
    }
    // 监听的事件不能超过了设置的最大监听数
    if (this._events[type].length === this.getMaxListeners()) {
        console.warn('警告-警告-警告');
    }
};
// 向前添加
EventEmitter.prototype.prependListener = function (type, cb) {
    this.on(type, cb, true);
};
EventEmitter.prototype.prependOnceListener = function (type, cb) {
    this.once(type, cb, true);
};
// 监听一次
EventEmitter.prototype.once = function (type, cb, flag) {
    // 先绑定,调用后删除
    function wrap() {
        cb(...arguments);
        this.removeListener(type, wrap);
    }
    // 自定义属性
    wrap.listen = cb;
    this.on(type, wrap, flag);
};
// 删除监听类型
EventEmitter.prototype.removeListener = function (type, cb) {
    if (this._events[type]) {
        this._events[type] = this._events[type].filter(listener => {
            return cb !== listener && cb !== listener.listen;
        });
    }
};
EventEmitter.prototype.removeAllListener = function () {
    this._events = Object.create(null);
};
// 返回所有的监听类型
EventEmitter.prototype.listeners = function (type) {
    return this._events[type];
};
// 发布
EventEmitter.prototype.emit = function (type, ...args) {
    if (this._events[type]) {
        this._events[type].forEach(listener => {
            listener.call(this, ...args);
        });
    }
};

module.exports = EventEmitter;

我的博客即将同步至腾讯云+社区,邀请大家一同入驻:https://cloud.tencent.com/dev...


如果觉得我的文章对你有用,请随意赞赏

你可能感兴趣的

言月 · 4月9日

老铁,请教一下你的实现,我认真看了下这个实现,觉得写得挺好的,不过有几处我自己没能理解的,还请多多指教:
移除事件的这块:

this._events[type] = this._events[type].filter(listener => {
            return cb !== listener && cb !== listener.listen;
        });

cb是用户要移除的事件处理器,type是用户要移除的事件类型,那么在once绑定的事件中,用户要移除的事件处理器被wrap了,用户不可能知道这个wrap的引用,而该实现是需要过滤掉这个wrap处理器的,在你的实现中cb !== listener过滤了非once绑定的和cb!==listener.listen过滤了once绑定的从而达到移除掉俩方面的该事件处理器。
用户如果绑定函数时,是通过函数引用绑定而不是匿名函数形式
比如:
var i = 0;
function clickItem(){
console.log(i++)
}
var clickItem2 = clickItem
this.on('click', clickItem, true)
this.once('click', clickitem2, true)
this.on('click', clickItem, false)
这样你once的执行之后,通过on添加的click的也都没了, 而用户可能希望事件执行器应该执行3次,而只移除once绑定的那个,其他click绑定的并不需要移除?当然我只是从逻辑角度去发现这个似乎存在有点问题,我举的这个例子的场景也存在考究是否存在真实使用的场景,另外我只是过来探讨一下,看看是否有更好的处理,我还没想出来,好的方案,所以提出来,看看大佬有咩有方案。
问题2: newListener这个类型是什么时候添加进去的呢,为什么会需要这个类型,我看on的时候做了这件事,但确没看到需要在什么时候给这个类型添加处理器。

回复

0

hello老铁,问题一的话,我觉得你once的执行并不会影响其他两个on的绑定,因为过滤掉的是cb === listener的值。而你once绑定的是clickItem2,测试代码如下
var ev = new EventEmitter()
var i = 0;
function clickItem() {

console.log(i++)

}
var clickItem2 = clickItem
ev.on('click', clickItem, true)
ev.once('click', clickItem2, true)
ev.on('click', clickItem, false)
ev.emit('click')
console.log(ev.listeners('click'))

结果如下
$ node Event.js
0
1
2
[ [Function: clickItem], [Function: clickItem] ]

问题二的话我只是截取了一部分业务代码,在更复杂的实现里有用到newListener,这个简单实现没有删掉,不好意思

SuperX 作者 · 4月23日
0

var i = 0;
var ev=new EventEmitter()
function clickItem(){
console.log(i++)
}
var clickItem2 = clickItem
ev.on('click', clickItem, true)
ev.once('click', clickitem2, true)
ev.on('click', clickItem, false)
我发现我的例子有问题,我补充一个,直接调用下面方法,会把所有监听器都移除掉。
ev.removeListener('click', clickItem2)//移除单一事件特定监听器
ev.emit('click')// 所有click监听器都没了,没成功调用

可能有点杠精。。。

言月 · 4月23日
载入中...