【JS】了解异步编程——事件发布/订阅模式的简单实现

2,930 阅读5分钟

事件发布/订阅模式

这是一种广泛应用于异步编程的模式,是回调函数的事件化,常常用来解耦业务逻辑。事件的发布者无需关注订阅的侦听器如何实现业务逻辑,甚至不用关注有多少个侦听器存在。数据通过消息的方式可以灵活的传递。 ——《深入浅出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是怎么实现的?为什么它也可以延迟处理?

传送门

  1. 【JS】5行代码实现bind函数
  2. CSS动画?教你使用障眼法,打造炫酷充电效果
  3. 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)         // 没有反应了,全被移除了