Skip to content

EventEmitter

什么是EventEmitter?

EventEmitter(事件派发器)是一个对事件进行监听的对象,简单来说就是为事件写回调函数,当触发一个事件执行后,会执行为该事件绑定的回调函数。

JavaScript 事件最核心的包括事件监听 (addListener)、事件触发 (emit)、事件删除 (removeListener),理解如下:

image.png

考虑一个DOM事件:

javascript
// Typescript
const button = document.querySelector('button');
button.addEventListener("click", (event) => {
    // do something with the event
})

我们向按钮单击事件添加了一个listener (监听器),并且已经订阅了一个正在被发出的事件,当事件发生时会触发回调。每次单击该按钮时,都会发出该事件,而该事件会触发回调。

当处理现有代码库时,或许需要触发自定义事件。不像单击按钮这样的特定DOM事件,而是假设想基于其他触发器发出一个事件,并得到一个事件响应。我们需要一个自定义事件派发器来实现这一点。

事件派发器是一种模式,它监听一个已命名的事件,触发回调,然后发出该事件并附带一个值。有时这被称为“发布/订阅”模型或监听器。它们指的是同一件事。

发布订阅模式

发布 — 订阅模式又叫观察者模式,它定义对象间的一种一对多的依赖关系,当一个对象的状 态发生改变时,所有依赖于它的对象都将得到通知。

在 JavaScript开发中,我们一般用事件模型来替代传统的发布 — 订阅模式。

实现的关键要素

  • 发布者有一个订阅者缓存队列
  • 发布者有增加和删除订阅者的方法
  • 发布者状态改变,需要notify方法通知队列中的所有订阅者
  • js中采用事件回调的方式来更新订阅者,因此订阅者不再需要update方法

下面来模拟下EventEmitter的初步实现,模拟vue中的Event bus

js
class EventEmitter {
    constructor() {
        this._events = {};
    }
	// 添加事件监听
    on(eventName, cb) {
        if (typeof cb !== "function") {
            return;
        }

        if (!this._events[eventName]) {
            this._events[eventName] = [];
        }
        this._events[eventName].push(cb);
    }
    // 触发事件
    emit(eventName, ...args) {
        this._events[eventName].forEach((item) => {
            item.apply(this, args);
        });
    }

    off(eventName, fn) {
        // 只传事件名,即删除该事件类型的所有事件函数
        if (!fn) {
            this._events[eventName] = null;
            delete this._events[eventName];
            return this;
        }

        const index = this._events[eventName.findIndex((cb) => cb === fn);
                                   
        if (index !== -1) {
            this._events[eventName].splice(index, 1);
        }
        return this;
    }
}

once函数

EventEmitter中的once方法可以做到绑定的事件只调用一次,之后不会再被调用,他的实现方式实在怎么样的?正常情况应该是在回调函数被调用一次之后移除这个回调。可以考虑在回调函数上加上once属性,在发起回调的时候判断once是否为真,来确定是否移除这个回调。这样可以达到目的,但是在发起回调时,需要每一次都判断,给通知方法增加了额外的负担,来考虑一个更聪明的实现方式。

javascript
once(eventName, cb) {
    const wrap = (...args) => {
        cb.apply(this, args);
        this.off(eventName, wrap);
    };
    wrap.cb = cb;
    this.on(eventName, wrap);

    return this;   // 返回自身,方便链式调用
}

将回调函数包裹起来,在包裹函数内部移除原回调函数,然后将wrap函数添加进观察者队列。同时要将原回调函数存进wrap中,用在在移除原回调时判断。

修改off方法

js
off(eventName, fn) {
    // 只传事件名,即删除该事件类型的所有事件函数
    if (!fn) {
        this._events[eventName] = null;
        delete this._events[eventName];
        return this;
    }
    const cbs = this._events[eventName];
	
    const index =  this._events[eventName].findIndex((cb) => {
        return cb === fn || cb.fn === fn;
    });
    if (index !== -1) {
        this._events[eventName].splice(index, 1);
    }
    return this;
}

修改emit方法

js
emit(eventName, ...args) {
    // 浅拷贝 存放数组副本,预防off后数组发生变化,导致调用错误
    const listeners = [...this._events[eventName]];
    if (!listeners) return;

    listeners.forEach((item) => {
        item.apply(this, args);
    });
}

newListener事件

比较有趣的是EventEmitter同时提供了newListener事件,每次添加观察者(即使是第二次添加newListener)时都会触发这个事件,在on方法中需要添加如下代码:

js
on(eventName, cb) {
    this.emit("newListener", eventName, cb);

    if (typeof cb !== "function") {
        return;
    }

    if (!this._events[eventName]) {
        this._events[eventName] = [];
    }
    this._events[eventName].push(cb);
}

若在 'newListener' 的回调中再次注册新监听器,会形成递归调用。源码通过临时标记防止无限循环:

js
constructor() {
    this._events = {};
    this._emitNewListener = false;
}
on(eventName, cb) {
    if (this._events.newListener && !this._emitNewListener) {
        this._emitNewListener = true; // 设置防递归标记
        this.emit("newListener", eventName, cb);
        this._emitNewListener = false; // 清除标记
    }

    if (typeof cb !== "function") {
        return;
    }

    if (!this._events[eventName]) {
        this._events[eventName] = [];
    }
    this._events[eventName].push(cb);
}

MIT Licensed