事件发布/订阅模式
这是一种广泛应用于异步编程的模式,是回调函数的事件化,常常用来解耦业务逻辑。事件的发布者无需关注订阅的侦听器如何实现业务逻辑,甚至不用关注有多少个侦听器存在。数据通过消息的方式可以灵活的传递。 ——《深入浅出Nodejs》
是的,了解JS的人都清楚,JS中动辄异步,动辄回调函数,尤其是在DOM中绑定事件。而目前广泛用于处理异步编程的方法主要有发布/订阅模式,Promise,async/await。
可是,你只知道传入回调函数,却不知道这中间都经历了什么?你只知道回调函数会在合适的时机被调用,可这又是为什么?为什么他传入之后可以延迟,而不是立即调用?带着这些疑问,看完下面这些代码,相信你会有所收获。
和观察者模式的区别
先看图:

实现
下面我们一起来写一个简易的事件类,用来注册事件或触发事件等:
class Event {
constructor() {
this.callback = {} // 储存事件
}
on () {} // 注册事件
once () {} // 注册只能调用一次的事件
emit () {} // 触发事件
del () {} // 移除指定的回调函数
}
首先,为什么回调函数可以延迟调用?因为内部可以将回调函数存储起来,所以我们首先需要一个对象来存放回调函数。
为什么是对象而不是数组?因为你可以注册很多事件呀,每个事件都可以有一个回调数组,存放一系列的回调函数。所以这里使用对象,可以通过不同的属性访问不同的事件队列。
代码本身不多,是因为加了详细的注释:
/**
* 注册事件
* @params type[String] 事件类型
* @params fn[Function] 回调函数
*/
on (type, fn) {
// 首先判断,callback对象有没有该事件的回调数组
if (!this.callback[type]) {
// 如果没有的话就新建一个数组用来存储type事件的回调函数
this.callback[type] = []
}
this.callback[type].push(fn) // 将回调函数fn存入数组
return this // 返回this是为了实现链式调用
}
/**
* 触发事件
* @params type[String] 事件类型
* @params ...params 传入的参数,不限个数
*/
emit (type, ...params) {
// 遍历执行对应的回调数组,并传入参数
this.callback[type].forEach(fn => fn(...params))
return this
}
/**
* 注册一个只能执行一次的事件
* @params type[String] 事件类型
* @params fn[Function] 回调函数
*/
once (type, fn) {
if (!this.callback[type]) {
this.callback[type] = []
}
let _this = this // 保存执行环境
// 由于只能执行一次,这里需要做点处理
// 注意该函数是有名字的,因为需要删除。但名字只在函数内部有效
this.callback[type].push(function once (...args) {
fn(...args) // 这里是为了方便emit传参
_this.del(type, once) // 执行一次后删除自己
})
return this // 链式调用
}
/**
* 删除对应的回调函数
* @params type[String] 事件类型
* @params fn[Function] 回调函数
*/
del (type, fn) {
// 利用filter删除数组中
this.callback[type] = this.callback[type].filter(cb => fn !== cb)
return this
}
这样我们就完成了一个自己的事件管理类,下面写点代码测试一下:
let event = new Event()
// 新建两个函数
let f1 = function (...args) {
console.log('参数', ...args)
}
let f2 = function () {
console.log('执行成功!')
}
event
.once('success', f1) // 注册f1函数,只能执行一次
.on('success', f2) // 注册f2函数
.emit('success', 12, 13) // 触发success,执行所有回调函数
.emit('success') // 这里只会执行f2,f1自动移除了
.del('success', f2) // 要删除函数的话就不能传入匿名函数了
.emit('success', 12) // 没有反应了,全被移除了
最后
怎么样?其实也不难吧。异步编程是JS的重头戏,理解它的原理是很有必要的。但是说到原理,我觉得在演示方面,挑出重点代码就行了。原生方法或者某些成熟的库一般会对参数等做校验,还要捕捉错误,进行错误处理。这样算下来代码量是很大的,不是很利于理解原理。原来就应该是清晰明了嘛,懂了原理不就可以自己拓展了。
另外感兴趣的朋友可以想一想Promise是怎么实现的?为什么它也可以延迟处理?
传送门
完整代码
class Event {
constructor() {
this.callback = {} // 储存事件
}
on (type, fn) {
if (!this.callback[type]) {
this.callback[type] = []
}
this.callback[type].push(fn)
return this
}
once (type, fn) {
if (!this.callback[type]) {
this.callback[type] = []
}
let _this = this
this.callback[type].push(function once (...args) {
fn(...args)
_this.del(type, once) // 执行一次后删除自己
})
return this
}
emit (type, ...params) {
this.callback[type].forEach(fn => fn(...params))
return this
}
del (type, fn) {
this.callback[type] = this.callback[type].filter(cb => fn !== cb)
return this
}
}
let event = new Event()
let f1 = function (...name) {
console.log('我的名字是:', ...name)
}
let f2 = function () {
console.log('执行成功!')
}
event
.once('success', f1) // 注册f1函数,只能执行一次
.once('success', f2) // 注册f1函数,只能执行一次
.on('success', f2) // 注册f2函数
.emit('success', 12, 13) // 触发success,执行所有回调函数
.emit('success') // 这里只会执行f2,f1自动移除了
.del('success', f2) // 要删除函数的话就不能传入匿名函数了
.emit('success', 12) // 没有反应了,全被移除了