EventEmitter的实现

前言

事件在js中非常的常见,不管是浏览器还是node,这种事件发布/订阅模式的应用都是很常见的。至于发布/订阅模式和观察者模式是否是同一种设计模式说法都有,这里不做具体的讨论。在之前的项目中也曾自己实现过一个事件模块,核心还是一个EventEmitter。下文就要结合node中的event模块分析一下,一个EventEmitter应该如何实现,有什么注意点。
源码地址
https://github.com/nodejs/nod...

基础的结构和设计

首先第一步就是一个EventEmitter的类,然后考虑一下这个类的实例属性和实例方法。
实例属性的话最基础的就是一个eventMap,可以是一个空对象,当然也可以这样创建Object.create(null)。如果需要还可以增加maxListener之类的属性。
实例方法的话,最核心的就是add delete emit分别是添加事件,删除事件,发布事件。当然实际实现的时候,例如count,has,once(一次性添加),preAdd(添加在事件队列最前面),这些方法则是可以根据实际需求去添加。

具体实现及注意点

以下代码均为简化的伪代码

add方法

EventEmitter.prototype.add = function(type, fn) {
    if (!isFunction(fn)) return;//判断是否在监听中添加的是合法的函数
    //判断type是否添加过,添加过一个还是多个函数
    if (this.event[type]) {
        if (isArray(this.event[type])){
            //如果想要实现preadd将push改为unshift即可
            this.event[type].push(fn);
        } else {
            //如果想要实现preadd改变顺序
            this.event[type] = [this.event[type], fn];
        }
    } else {
        this.event[type] = fn;
    }
}

once方法

参考一下node的once方法

function onceWrapper(...args) {
  if (!this.fired) {
    this.target.removeListener(this.type, this.wrapFn);
    this.fired = true;
    Reflect.apply(this.listener, this.target, args);
  }
}

function _onceWrap(target, type, listener) {
  var state = { fired: false, wrapFn: undefined, target, type, listener };
  var wrapped = onceWrapper.bind(state);
  wrapped.listener = listener;
  state.wrapFn = wrapped;
  return wrapped;
}

EventEmitter.prototype.once = function once(type, listener) {
  this.on(type, _onceWrap(this, type, listener));
  return this;
};

函数用onceWrap包裹,运行前需要对添加的监听进行移除

delete

很简单理清楚几种边界情况就可以了

EventEmitter.prototype.delete = function(type, fn) {
    //直接删除整类监听
    if(fn === undefined){
        this.events[type] && delete this.events[type];
    }else{
        //判断fn合法性就省了
        if(this.events[type]) {
            if (this.events[type] === fn) {
                delete this.events[type];
            } else {
                for (var i in this.events[type]) {
                    if(this.events[type][i] === fn){
                        if (i === 0) {
                            this.events[type].shift();
                        } else {
                            this.events[type].splice(i,1);
                        }
                    }
                }
                if(this.events[type].length === 1) this.events[type] = this.events[type][0];
            }
        }
    }
    
}

emit

EventEmitter.prototype.emit = function(type) {
    //获取参数
    var args = [].slice.call(arguments, 1);
    var handler = events[type];

    if (handler === undefined) return false;

    if (typeof handler === 'function') {
        handle.apply(this, args);
    } else {
        var len = handler.length;
        const listeners = arrayClone(handler, len);
        for (var i = 0; i < len; ++i)
             handle[i].apply(this, args);
   }
}

发布事件有两个注意点,一个是注意参数的保留,另一个则是上下文,这里上下文是直接取了event实例的上下文,也可以考虑一下手动传入上下文的形式,或者说fn在定义的时候直接写成箭头函数,也可以避免上下文成为eventEmit实例的上下文。

错误处理

这里提一下node的event的错误事件

当 EventEmitter 实例中发生错误时,会触发一个 'error' 事件。 这在 Node.js 中是特殊情况。
如果 EventEmitter 没有为 'error' 事件注册至少一个监听器,则当 'error' 事件触发时,会抛出错误、打印堆栈跟踪、且退出 Node.js 进程。
为了防止 Node.js 进程崩溃,可以在 process 对象的 uncaughtException 事件上注册监听器,或使用 domain 模块。 (注意,domain 模块已被废弃。)
作为最佳实践,应该始终为 'error' 事件注册监听器。

如果有需要在自己的实践中也可以增加一个错误处理的机制,保证event实例的稳定性

总结

一个事件订阅发布类其实不难实现,而在node中有很多厉害的类都是继承的事件类,而之后我会接着对node文件系统进行学习

你可能感兴趣的:(javascript,node.js,event)