手写事件总线 EventEmitter(发布订阅模式)
我们去搜索 24 种基本的设计模式,会发现其中并没有发布订阅模式;刚开始发布订阅模式只是观察者模式的一个别称,但是经过时间的沉淀,他改进了观察者模式的缺点,渐渐地开始独立于观察者模式。
我们也来看一下它的一个定义:
发布订阅模式是基于一个事件(主题)通道,希望接收通知的对象
Subscriber
通过自定义事件订阅主题,被激活事件的对象Publisher
通过发布主题事件的方式通知各个订阅该主题的Subscriber
对象。
有三种角色:发布者(Publisher)、订阅者(Subscriber)、调度中心/事件中心
观察者模式 vs 发布订阅模式
观察者模式 | 发布订阅模式 |
---|---|
2 个角色 | 3 个角色 |
重点是被观察者 | 重点是调度中心 |
- 观察者模式把观察者对象维护在目标对象中的,需要发布消息时直接发消息给观察者。在观察者模式中,目标对象本身是知道观察者存在的。
- 而发布/订阅模式中,发布者并不维护订阅者,也不知道订阅者的存在,所以也不会直接通知订阅者,而是通知调度中心,由调度中心通知订阅者。
在代码中发现有 watch
、watcher
、observe
、observer
、listen
、listener
、dispatch
、trigger
、emit
、on
、event
、eventbus
、EventEmitter
这类单词出现的地方,很有可能是在使用观察者模式或发布订阅的思想。不妨点进它的源码实现看看其他 coder 在实现观察者模式或发布订阅时有哪些巧妙的细节。
观察者模式实现
js
// 发布者
class Publisher {
constructor(name, context) {
this.name = name
this.context = context
}
publish(type, content) {
this.context.publish(type, content)
// 当发布者发布消息时,通过调度中心,通知所有订阅者
this.context.notify(type)
}
}
// 订阅者
class Subscriber {
constructor(name, context) {
this.name = name
this.context = context
}
subscribe(type, cb) {
this.context.subscribe(type, cb)
}
}
// 发布订阅中心
class PubSub {
constructor() {
this.messages = {}
this.listeners = {}
}
publish(type, content) {
const existContent = this.messages[type]
if (!existContent) {
this.messages[type] = []
}
this.messages[type].push(content)
}
subscribe(type, cb) {
const existListener = this.listeners[type]
if (!existListener) {
this.listeners[type] = []
}
this.listeners[type].push(cb)
}
notify(type) {
const messages = this.messages[type]
const subscribers = this.listeners[type] || []
subscribers.forEach((cb) => cb(messages))
}
}
// testing
const TYPE_MUSIC = 'music'
const TYPE_MOVIE = 'movie'
const TYPE_NOVEL = 'novel'
const pubsub = new PubSub()
const publisherA = new Publisher('publisherA', pubsub)
const publisherB = new Publisher('publisherB', pubsub)
const publisherC = new Publisher('publisherC', pubsub)
publisherA.publish(TYPE_MUSIC, 'we are young')
publisherA.publish(TYPE_MOVIE, 'the silicon valley')
publisherB.publish(TYPE_MUSIC, 'stronger')
publisherC.publish(TYPE_MOVIE, 'imitation game')
const subscriberA = new Subscriber('subscriberA', pubsub)
subscriberA.subscribe(TYPE_MUSIC, (res) => {
console.log('subscriberA received', res)
})
const subscriberB = new Subscriber('subscriberB', pubsub)
subscriberB.subscribe(TYPE_NOVEL, (res) => {
console.log('subscriberB received', res)
})
const subscriberC = new Subscriber('subscriberC', pubsub)
subscriberC.subscribe(TYPE_MOVIE, (res) => {
console.log('subscriberC received', res)
})
pubsub.notify(TYPE_MUSIC)
pubsub.notify(TYPE_MOVIE)
pubsub.notify(TYPE_NOVEL)
实现一个发布订阅模式
实现一个发布订阅模式,包含以下方法
- on
- emit
- once
- off
js
// 事件总线(发布订阅模式)
class EventEmitter {
constructor() {
this.events = {}
}
on(name, fn) {
const fns = this.events[name] || []
fns.push({
name,
fn
})
this.events[name] = fns
return this
}
off(name, fn) {
const fns = this.events[name] || []
const liveFns = []
for (const item of fns) {
if (item.fn !== fn && item.fn._ !== fn) {
liveFns.push(item.fn)
}
}
this.events[name] = liveFns
return this
}
emit(name, ...rest) {
// 创建副本,如果回调函数内继续注册相同事件,会造成死循环
const fns = [...(this.events[name] || [])]
for (const item of fns) {
item.fn(...rest)
}
return this
}
once(name, fn) {
const self = this
function listener() {
self.off(name, fn)
fn(...arguments)
}
listener._ = fn
return this.on(name, listener)
}
}
// testing
const eventBus = new EventEmitter()
const fn1 = (name, age) => {
console.log(`${name}'s age is ${age}`)
}
const fn2 = (name, age) => {
console.log(`hello, ${name}'s age is ${age}`)
}
eventBus.on('say', fn1)
eventBus.on('say', fn1)
eventBus.once('say', fn2)
eventBus.emit('say', 'jack', 20)
参见:tiny-emitter
js
// 源码 https://github.com/scottcorgan/tiny-emitter/blob/master/index.js
function E() {
// Keep this empty so it's easier to inherit from
// (via https://github.com/lipsmack from https://github.com/scottcorgan/tiny-emitter/issues/3)
}
E.prototype = {
on: function (name, callback, ctx) {
var e = this.e || (this.e = {})
;(e[name] || (e[name] = [])).push({
fn: callback,
ctx: ctx
})
return this
},
once: function (name, callback, ctx) {
var self = this
function listener() {
self.off(name, listener)
callback.apply(ctx, arguments)
}
listener._ = callback
return this.on(name, listener, ctx)
},
emit: function (name) {
var data = [].slice.call(arguments, 1)
var evtArr = ((this.e || (this.e = {}))[name] || []).slice()
var i = 0
var len = evtArr.length
for (i; i < len; i++) {
evtArr[i].fn.apply(evtArr[i].ctx, data)
}
return this
},
off: function (name, callback) {
var e = this.e || (this.e = {})
var evts = e[name]
var liveEvents = []
if (evts && callback) {
for (var i = 0, len = evts.length; i < len; i++) {
if (evts[i].fn !== callback && evts[i].fn._ !== callback)
liveEvents.push(evts[i])
}
}
// Remove event from queue to prevent memory leak
// Suggested by https://github.com/lazd
// Ref: https://github.com/scottcorgan/tiny-emitter/commit/c6ebfaa9bc973b33d110a84a307742b7cf94c953#commitcomment-5024910
liveEvents.length ? (e[name] = liveEvents) : delete e[name]
return this
}
}
module.exports = E
module.exports.TinyEmitter = E
对比分析
mitt 和 tiny-emitter 的对比分析
- 共同点
- 都支持 on(type, handler)、off(type, [handler]) 和 emit(type, [evt]) 三个方法来注册、注销、派发事件
- 不同点
- emit
- 有
all
属性,可以拿到对应的事件类型和事件处理函数的映射对象,是一个Map
不是 {} - 支持监听
'*'
事件,可以调用emitter.all.clear()
清除所有事件 - 返回的是一个对象,对象存在上面的属性
- 有
- tiny-emitter
- 支持链式调用,通过
e
属性可以拿到所有事件(需要看代码才知道) - 多一个
once
方法,并且支持设置this
(指定上下文ctx
) - 返回的一个函数实例,通过修改该函数原型对象来实现的
- 支持链式调用,通过
- emit