场景
存在如下场景:当音频播放器分别处于加载资源、播放音频、播放结束等不同状态时,通过某种机制执行不同操作(如更新 UI 状态)。即通过监听某些对象,当其状态发生变化时,触发不同事件。
上述场景可通过 EventEmitter 实现,该方法至少可对应两种模式:观察者模式与发布/订阅模式。我们先来了解这两种模式:
观察者模式
观察者模式(Observer Pattern)定义了 1:n 关系,使 n 个观察者对象监听某个被观察者对象(主题对象),当被观察者对象状态发生变化时,它将主动通知所有观察者对象这个消息,不同的观察者对象将产生不同动作。
这种行为型模式关注的是观察者与被观察者之间的通讯,发布/订阅模式由它衍生而来。
代码实现
class Subject {
constructor() {
this.observers = []
}
add(observer) {
this.observers.push(observer)
}
notify(...args) {
this.observers.forEach((observer) => observer.update(...args))
}
}
class Observer {
update(...args) {
console.log(...args)
}
}
使用方式
// 创建 2 个观察者 observer1、observer2
const observer1 = new Observer()
const observer2 = new Observer()
// 创建 1 个目标 subject
const subject = new Subject()
// 目标 subject 主动添加 2 个观察者,建立 subject 与 observer1、observer2 依赖关系
subject.add(observer1)
subject.add(observer2)
doSomething()
// 目标触发某些事件、主动通知观察者
subject.notify('subject fired a event')
由于观察者模式没有调度中心,观察者必须将自身添加到被观察者中进行管理;被观察者触发事件后,将主动通知每个观察者。
发布/订阅模式
发布订阅模式(Pub/Sub Pattern)与观察者模式主要区别是:前者具有事件调度中心,它将过滤发布者发布的所有消息并分发给订阅者,发布者与订阅者无需关心对方是否存在。
代码实现
class PubSub {
constructor() {
this.subscribers = []
}
subscribe(topic, callback) {
const callbacks = this.subscribers[topic]
if (callbacks) {
callbacks.push(callback)
} else {
this.subscribers[topic] = [callback]
}
}
publish(topic, ...args) {
const callbacks = this.subscribers[topic] || []
callbacks.forEach((callback) => callback(...args))
}
}
使用方式
// 创建事件调度中心,为发布者与订阅者提供调度服务
const pubSub = new PubSub()
// A 系统订阅了 SMS 事件,不关心谁将发布这个事件
pubSub.subscribe('lovelyEvent', console.log)
// B 系统订阅了 SMS 事件,不关心谁将发布这个事件
pubSub.subscribe('lovelyEvent', console.log)
// C 系统发布了 SMS 事件,不关心谁会订阅这个事件
pubSub.publish('lovelyEvent', 'I just published a lovely event')
两种模式相比,发布/订阅模式有利于系统间解耦,适合处理跨系统消息事件。
EventEmitter
EventEmitter 是 Node.js events 模块中的类,用于对 Node.js 中事件进行统一管理。通过查看其源码实现,我们可以尝试构造自己的 EventEmitter 来满足文首场景,以下实现为发布/订阅模式的体现。
EventEmitter 实例具有如下几个主要方法:
代码实现
class EventEmitter {
constructor() {
this._events = Object.create(null)
}
/**
* 为 EventEmitter 实例添加事件
*/
addListener(eventName, listener) {
if (this._events[eventName]) {
this._events[eventName].push(listener)
} else {
this._events[eventName] = [listener]
}
return this
}
/**
* 移除某个事件
* 调用该方法时,仅移除一个 Listener,参考下方 Case#7
*/
removeListener(eventName, listener) {
if (this._events[eventName]) {
for (let i = 0; i < this._events[eventName].length; i++) {
if (this._events[eventName][i] === listener) {
this._events[eventName].splice(i, 1)
// NOTE: 或调用 spliceOne 方法,参见文末
// spliceOne(this._events[eventName], i);
break
}
}
}
return this
}
/**
* 移除指定事件名中的事件或全部事件
*/
removeAllListeners(eventName) {
if (this._events[eventName]) {
this._events[eventName] = []
} else {
this._events = Object.create(null)
}
}
/**
* 是 addListener 方法别名
*/
on(eventName, listener) {
return this.addListener(eventName, listener)
}
/**
* 类似 on 方法,但通过 once 方法注册的事件被多次调用时只执行一次
*/
once(eventName, listener) {
let fired = false
let onceWrapperListener = (...args) => {
this.off(eventName, onceWrapperListener)
if (!fired) {
fired = true
listener.apply(this, args)
}
}
return this.on(eventName, onceWrapperListener)
}
/**
* 是 removeListener 方法别名
*/
off(eventName, listener) {
return this.removeListener(eventName, listener)
}
/**
* 主动触发事件
*/
emit(eventName, ...args) {
if (this._events[eventName]) {
for (let i = 0; i < this._events[eventName].length; i++) {
this._events[eventName][i].apply(this, args)
}
}
return this
}
}
使用方法
const emitter = new EventEmitter()
const fn1 = (name) => console.log(`fn1 ${name}`)
const fn2 = (name) => console.log(`fn2 ${name}`)
/**
* Case 1
*/
emitter.on('event1', fn1)
emitter.on('event1', fn2)
emitter.emit('event1', 'ju')
// Expected Output:
// fn1 ju
// fn2 ju
/**
* Case 2
*/
emitter.once('event1', fn1)
emitter.emit('event1', 'ju')
emitter.emit('event1', 'ju')
emitter.emit('event1', 'ju')
// Expected Output:
// fn1 ju
/**
* Case 3
*/
emitter.on('event1', fn1)
emitter.on('event1', fn1)
emitter.off('event1', fn1)
emitter.on('event1', fn1)
emitter.emit('event1', 'ju')
// Expected Output:
// fn1 ju
// fn1 ju
/**
* Case 4
*/
emitter.on('event1', fn1)
emitter.on('event1', fn2)
emitter.on('event2', fn2)
// emitter.removeAllListeners('event1');
emitter.emit('event1', 'ju')
emitter.emit('event2', 'zhiyuan')
// Expected Output:
// fn1 ju
// fn2 ju
// fn2 zhiyuan
/**
* Case 5
*/
emitter.on('event1', fn1)
emitter.on('event1', fn2)
emitter.on('event2', fn2)
emitter.removeAllListeners('event1')
emitter.emit('event1', 'ju')
emitter.emit('event2', 'zhiyuan')
// Expected Output:
// fn2 zhiyuan
/**
* Case 6
*/
emitter.on('event1', fn1)
emitter.on('event1', fn2)
emitter.on('event2', fn2)
emitter.removeAllListeners()
emitter.emit('event1', 'ju')
emitter.emit('event2', 'zhiyuan')
// Expected Output:
// (empty output)
/**
* Case 7
*/
emitter.on('event1', fn1)
emitter.on('event1', fn1)
emitter.off('event1', fn1)
emitter.on('event1', fn2)
emitter.emit('event1', 'ju')
// Expected Output:
// fn1 ju
// fn2 ju
需要注意的是,在调用 off 方法时,其传入的 Listener 函数应与 on 方法中传入的 Listener 函数一致,请勿传递匿名函数,否则 off 方法无效。这是由于在 JavaScript 中,(函数)对象是按引用传递的,两个逻辑一样的函数却并不相等。因此,我们先将函数定义好,再将该函数传入 on/off 方法。
另外,当实例触发一个事件时,所有与其相关的 Listener 将被同步调用(请查看 emit 方法实现:循环数组、依次被调用)。
其它
spliceOne 方法
在 events 官方实现中,调用 removeListener 方法移除某一个 Listener 函数时,调用了自定义的 spliceOne 方法,该方法执行效率优于 splice。实现如下:
function spliceOne(list, index) {
for (; index + 1 < list.length; index++) {
list[index] = list[index + 1]
}
list.pop()
}
return this
return this 返回了当前对象实例,在本文 EventEmitter 实现中,this 指代 emitter,这样可以实现链式调用。
const emitter = new EventEmitter()
const fn = () => console.log('Hello')
emiter.on('event', fn).emit('event')
// or
emiter.once('event', fn).emit('event')