这是一种广泛应用于异步编程的模式,是回调函数的事件化,常常用来解耦业务逻辑。事件的发布者无需关注订阅的侦听器如何实现业务逻辑,甚至不用关注有多少个侦听器存在。数据通过消息的方式可以灵活的传递。 ——《深入浅出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是怎么实现的?为什么它也可以延迟处理?
传送门
【JS】5行代码实现bind函数CSS动画?教你使用障眼法,打造炫酷充电效果JS动画?其实很简单。150行代码,带你制作雪花飞舞特效
完整代码
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) // 没有反应了,全被移除了