手写实现一个浏览器端的EventEmitter

1,806 阅读5分钟

「这是我参与2022首次更文挑战的第3天,活动详情查看:2022首次更文挑战」。

EventEmitter是什么

javascript这门语言,除了前端我们常用的DOM(描述处理网页内容的方法和接口)、BOM(描述与浏览器进行交互的方法和接口)等,还有用于后端的Node.js,其中内置的各种模块里,events就是其中比较重要的一个模块,因为Node.js的事件驱动机制就是建立在events模块上,许多需要实现异步事件驱动架构Node.js模块都内置了events,掌握其实现原理还是非常重要的。
events模块对外提供了一个EventEmitter对象,用于对Node中的事件进行统一管理。EventEmitter对象上的一些重要方法和功能如下。

  • addListener(event,listener),给指定事件添加一个监听器,on方法的别名。
  • emit(event,[arg1],[arg2],[arg3]...),根据参数的顺序执行每个监听器。
  • removeListener(event,listener),移除指定事件的某个监听器,off方法的别名。
  • removeAllListener([event]),移除指定事件的所有监听器,如果没有指定事件,则是移除所有事件的监听器。
  • once(event,listener),为指定事件注册一个一次性的监听器,监听器触发一次后立即移除该监听器。
  • listeners(event),返回指定事件的监听器数组。

EventEmitter的用法

介绍了这么久,我们赶紧看看EventEmitter该如何使用,其实很简单,代码如下。

// 定义三个监听器函数
function hello_1(name){
    console.log('hello',name);
}
function hello_2(name){
    console.log('HELLO',name);
}
function hello_3(name){
    console.log('hello HELLO',name);
}
const events = require('events');
let eventEmitter = new events.EventEmitter();
eventEmitter.addListener('hello',hello_1);
eventEmitter.addListener('hello',hello_2);
eventEmitter.once('hello',hello_3);
eventEmitter.emit('hello','xiaomi'); 
// 输出 hello xiaomi
// 输出 HELLO xiaomi
// 输出 hello HELLO xiaomi
eventEmitter.removeListener('hello',hello_1); // 移除hello事件对应的hello_1监听器
eventEmitter.emit('hello','xiaomi'); 
// 只输出 HELLO xiaomi ,因为once,所以hello_3触发一次后就被移除了
eventEmitter.removeAllListener('hello'); // 移除hello事件对应的所有监听器
eventEmitter.emit('hello','xiaomi'); 
// 没有任何输出,说明移除了hello事件的所有监听器

通过new实例化一个EventEmitter对象,然后通过实例上的addListener方法监听hello事件并定义一个对应的执行函数,用once定义一个只触发一次的监听器,再通过emit方法派发hello事件通知执行函数执行,最后移除hello事件的监听器。这就是一般使用方式,用到了addListener、once、emit、removeListener、removeAllListener等方法,这里补充一个知识点,添加一个事件监听,在页面销毁的时候就应该移除这个事件监听,以免造成不必要的内存浪费,比如上面的addListener 和 removeListener,这类似的还有比如on 和 off、subscribe 和 unsubscribe、setInterval 和 clearInterval等。
看完使用方式,大家一起思考下,EventEmitter应用了什么设计模式?以及这样的方式组织代码有什么益处? 答案我们在最后再补上。

手写实现一个EventEmitter

结合上面的介绍,我们先来实现一个浏览器端的基础版EventEmitter,其中包括addListener、once、emit、removeListener、removeAllListener这些方法。

class EventEmitter {
    constructor(){
        // 初始化__events对象,用于存放自定义事件和对应的回调函数
        /*
        对象结构:{
            'hello':[{listener: function,once: boolean},...]
        }
        */
        this.__events = {};
    }

    addListener(event,listener){
        if (!event || !listener) return;
        if (!isValidListener(listener)) {
            throw new TypeError('listener must be a function');
        }
        let events = this.__events;
        let listeners = (events[event] = events[event] || []);
        // 用于判断是否是对象,如果是函数则包裹成目标对象
        let listenerIsWrapped = typeof listener === 'object';
        // 排重,相同的监听器只需要添加一次
        if (indexOfListener(listeners, listener) === -1) {
            listeners.push(
                listenerIsWrapped ? listener : {
                    listener: listener,
                    // once 属性用于判断处理once机制
                    once: false
                }
            );
        }
        return this;
    }

    removeListener(event,listener){
        let listeners = this.__events[event];
        if (!listeners) return;
        let index;
        for (let i = 0, len = listeners.length; i < len; i++) {
            if (listeners[i] && listeners[i].listener === listener) {
                index = i;
                break;
            }
        }
        // 删除__events中对应的监听器
        if (typeof index !== 'undefined') {
            listeners.splice(index, 1);
        }
        return this;
    }

    removeAllListener(event){
        // 如果该 event 存在,则将其对应的 listeners 的数组直接清空
        if (event && this.__events[event]) {
            this.__events[event] = [];
        } else { // 全部清空
            this.__events = {};
        }
    }

    once(event,listener){
        // 直接调用 addListener 方法,once 参数传入 true,待执行之后进行 once 处理
        return this.addListener(event, {
            listener: listener,
            once: true
        });
    }

    emit(event,...args){
        // 直接通过内部对象获取对应自定义事件的回调函数数组
        let listeners = this.__events[event];
        if (!listeners) return;
        // 需要考虑多个 listener 的情况
        listeners.forEach(listener => {
            listener.listener.apply(this, args || []);
            // 给 listener 中 once 为 true 的进行特殊处理
            if (listener.once) {
                this.removeListener(event, listener.listener);
            }
        });
        return this
    }
}

// 判断是否是合法的 listener
function isValidListener(listener) {
    if (typeof listener === 'function') {
        return true;
    } else if (listener && typeof listener === 'object') {
        return isValidListener(listener.listener);
    } else {
        return false;
    }
}

// 查找新增自定义事件在__events里的位置,-1则说明不存在
function indexOfListener(array, item) {
    let result = -1;
    item = typeof item === 'object' ? item.listener : item;
    for (let i = 0, len = array.length; i < len; i++) {
        if (array[i].listener === item) {
            result = i;
            break;
        }
    }

    return result
}

从代码中可以看出addListener的实现思路就是当调用订阅一个自定义事件的时候,只要该事件校验成功后,就把该自定义事件 push到this.__events这个对象中存储,等emit发布的时候,则直接获取__events中对应事件的listener回调函数,而后直接执行该回调函数就能实现想要的效果。emit方法其实就是通过__events拿出对应自定义事件的回调函数数组遍历执行,在执行过程中对once选项为true的情况,额外触发removeListener方法移除该自定义事件的该回调函数,从而实现自定义事件只执行一次的效果。once方法也就是执行一次addListener方法,不过增加了个once为true的标志,在emit时做额外的处理。removeListener和removeAllListener也很简单,就是根据__events对象完成删除或者清空的操作。

测试结果

微信图片_20220209024044.png

微信图片_20220209024037.png

如图所示用我们手写的EventEmitter,替换上面例子中的events.EventEmitter,输出结果也是一样的。

总结

今天我们一起学习了EventEmitter的相关api,最后也手写了一个浏览器端简版的EventEmitter。回到实现前抛出的两个问题,其实不难发现EventEmitter正是采用了在前端非常常见的发布-订阅模式。发布-订阅模式的优点就是两个对象间不再是显性的调用,在完全不清楚双方细节的情况下依旧可以完成通信,实现代码的松耦合。但也不是一点缺点也没有,在弱化了对象间的通信的细节同时,如果过度使用,代码的可读性和易维护性也会有所降低!