又叫观察者模式。
定义对象间的一种一对多的依赖关系,当一个对象的状态发生改变时,所有依赖于它的对象都会得到通知。
- 发布订阅模式的作用
- DOM事件
- 自定义时间
- 发布订阅的通用实现
- 取消订阅事件
- 真实例子
- 网站订阅
- 全局的发布订阅对象
- 模块间通信
发布订阅模式的作用
一个例子,现实生活中,如果你要买房的话,会留下联系方式给房产中介,当你关注的楼盘一有消息,中介就会打电话通知你。
这就是现实生活中的发布订阅模式。可以发现,这种模式有显著的优点:
- 订阅者不用时刻给发布者发起请求,而是由发布者在适当的时机(某个条件达成时)通知订阅者。
- 两者不再有强耦合,当有新的订阅者出现时,只需要向发布者添加即可。
上述两点中,第一点可以广泛应用于异步编程中,是一种代替传递回调函数的方案。比如接口请求的success和error状态,或者动画每一帧完成时去调用一个函数。
第二点可以取代对象之间硬编码的通知机制,一个对象不再显式的调用另一个对象的某个接口。
DOM事件
在DOM节点上绑定事件函数,就是一种发布订阅模式。
document.body.addEventListener('click', function() {
console.log('1')
}, false)
上述代码将click事件绑定在body中,当点击事件触发时,就会执行这个函数。
同样的,还可以随意新增删除订阅者。都不会影响发布者代码的编写。
document.body.addEventListener('click', function() {
console.log('2')
}, false)
document.body.addEventListener('click', function() {
console.log('3')
}, false)
会依次执行绑定在body上的事件,这些事件(订阅者)之间不会互相影响。
自定义事件
如何实现发布订阅模式?
- 确定谁是发布者
- 定义一个缓存列表,用来存放回调函数以通知订阅者
- 发布消息时,遍历缓存列表,依次触发存放的回调函数
也可以向回调函数中传入一些参数,订阅者接受这些参数,并可以自行处理。
let salesOffices = {} // 确定该对象作为发布者
salesOffices.clientList = [] // 为发布者添加一个缓存列表
salesOffices.listen = function(fn) { // 为发布者添加监听函数,用来向缓存列表中存放回调函数
this.clientList.push(fn)
}
salesOffices.trigger = function() { // 为发布者添加触发函数,用来发布消息
for(let i = 0, fn; fn = this.clientList[i++]) {
fn.apply(this, arguments) // arguments是发布时携带的参数
}
}
向发布者订阅消息
salesOffices.listen(function (price, meter) {
console.log(price, meter, 'A')
})
salesOffices.listen(function (price, meter) {
console.log(price, meter, 'B')
})
发布者发布消息
salesOffices.trigger(3000, 100) // 3000, 100, A
salesOffices.trigger(4000, 80) // 4000, 80, B
以上就是一个简单的发布订阅模式的代码,但依然存在于一些问题,订阅者只想知道自己感兴趣的消息,而上述程序中,不论订阅者是否感兴趣,只要发布者触发了消息通知,所有订阅者都会收到消息。
所以为listen方法添加一个key,作为某一类订阅信息的标志。
添加了订阅类型的key后,原来clientList由数组改为对象,它应该具有以下数据结构
let clientList = {
type1: [],
type2: [fn1, fn2]
}
所以listen方法的逻辑应该改写为:
salesOffices.listen = function(key, fn) {
if(!this.clientList[key]) {
this.clientList[key] = []
}
this.clientList[key].push(fn)
}
而trigger方法则改写为:
salesOffices.trigger = function () {
let key = Array.prototype.shift.call(arguments)
let fns = this.clientList[key] // 根据key值获取对应的订阅类型的缓存列表
if(!fns || fns.length === 0) { // 如果未曾订阅过该类型的函数,则直接返回false
return false
}
for(let i = 0, fn; fn = fns[i++]) { // 发布消息
fn.apply(this, arguments)
}
}
由于arguments是一个类数组,本身并没有Array的方法,所以使用call来“借用”Array的shift方法,来获取函数中第一个形参。
这就实现了订阅者只会收到自己感兴趣的通知。
发布订阅模式的通用实现
将发布订阅的功能提取出来,放在一个单独的对象。
let event = {
clientList: {},
listen: function(key, fn) {
// 同以上listen逻辑
},
trigger: function() {
// 同以上trigger逻辑
}
}
实现一个动态安装模式的函数,将event中的事件和值混入到目标对象中(这里有可能造成将原有目标函数中重名key的逻辑覆盖)。
let installEvent = function(obj) {
for(let key in event) {
obj[key] = event[key]
}
}
下面定义一个要作为发布者的对象,然后通过installEvent为该对象添加发布订阅功能
let salesOffices = {}
installEvent(salesOffices)
这样salesOffices就作为一个发布订阅对象而存在了。
取消订阅事件
既然有订阅事件的函数,那么也应该有取消订阅事件的函数。根据上面通用发布订阅的实现,继续向event对象中新增一个remove函数。该函数接受两个参数。
- key,需要取消订阅的类型
- fn,需要取消的订阅函数
该函数的基本逻辑是
- 检查在缓存列表中是否存在该类型的订阅
- 如果没有,则返回false
- 检查是否传入了fn
- 如果存在,则取消对应的订阅
- 如果不存在,则取消全部订阅
let event = {
remove: function(key, fn) {
let fns = this.clientList[key] // 获取对应订阅类型的缓存列表
if(!fns) {
return false
}
if(!fn) { // 如果没有传入fn,就清除对应订阅类型的缓存列表
fns.length = 0
} else { // 如果传入fn,则遍历对应订阅类型的缓存列表,splice对应的订阅
for(let l = fns.length - 1; l >= 0; l--) {
let _fn = fns[l]
if(_fn === fn) {
fns.splice(1, l)
}
}
}
}
}
真实例子
网站登录
全局的发布订阅对象
之前通用发布订阅中,存在一些问题
- 为每个发布者都添加了listen和trigger方法,和一个clientList。有些浪费资源
- 订阅者和发布者之间还是有一些耦合,订阅者需要知道发布者的名字才行。
如果订阅者还需要订阅其他发布者的信息,还需要额外再写一次salesOffices.listen('typeA', function(price) { /* ... */ })
发布订阅模式可以使用全局的Event对象来实现。otherOffices.listen('typeB', function(price) {})
let Event = (function () {
let clientList = {},
listen,
trigger,
remove
// listen trigger remove相关函数逻辑
return {
listen,
trigger,
remove
}
})()