手写事件总线 EventEmitter(发布订阅模式)
实现一个发布订阅模式拥有 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
观察者模式 vs 发布订阅模式
观察者模式 | 发布订阅模式 |
---|---|
2 个角色 | 3 个角色 |
重点是被观察者 | 重点是发布订阅中心 |