介绍
发布-订阅模式, 定义对象间一对多的依赖关系,当一个对象状态发生改变,依赖于它的对象可以收到通知,并做出改变
-
现实中的 比如你女盆友在某东的抢购中看到一只口红, 很心水, 灰常高兴的点进去,发现当前区域无货,但是又不想放弃,机智的她问询问客服什么时候到货,客服balabala一大堆,告诉可以在商品详情页面点击[到货通知],在货源充足时,会收到到货通知的信息,果不其然,在等待个把星期后,在快熄灯的时候收到了到货信息.
-
在以上这个例子中, 你女盆友作为订阅者, 某东作为发布者,当货源到时,某东会取出订阅记录,通知所有订阅者,心心念的口红到了,可以剁手啦
-
常见场景 vue中的数据绑定 vue的on和emit
-
特点 广泛应用于异步编程中(替代了传递回调函数) 对象之间松散耦合的编写代码
自定义事件
定义一个Shop类,可以添加订阅者,在发布消息后,调用订阅者做出响应动作
// 定义Shop类
class Shop {
constructor() {
// 存储订阅者的回调函数
this.subs = []
}
// 订阅
on(fn) {
// 添加订阅者的回调函数
this.subs.push(fn)
}
// 发布信息
emit() {
// 取出订阅者的回调函数, 调用函数,完成通知
this.subs.forEach(sub => {
sub.apply(this, arguments)
})
}
}
使用如下
// 实例化Shop
const shop = new Shop()
// 订阅者订阅消息
shop.on((day, price) => {
console.log(`您期待天到货${day}, 期待价格是${price}`)
})
shop.on((num) => {
console.log(`您期数量大于${num}`)
})
setTimeout(() => {
shop.emit(30, 998)
shop.emit('端茶和倒水', '足球')
}, 3000)
结果如下:
您期待天到货30, 期待价格是998
您期数量大于30
您期待天到货20, 期待价格是undefined
您期数量大于20
上述代码存在一个问题, 任意一个消息发出(有的订阅者只希望在指定价格收到通知,有的希望数量大于指定量收到通知), 所有订阅者都会收到消息, 体验不太好, 订阅者只关心想要关心的内容
改进
怎么样只接受自己想要的信息,需要不同消息进行分类,在订阅的时候,告诉对方消息的类型,在触发的时候,也告诉消息和详细的类型
修改存储订阅者的容器subs, 改用对象, 使得可以存储类型, 类型的值对应存储自己的订阅者
在订阅时,根据订阅的类型,存储订阅者的函数
发布消息时, 取出类型, 调用该类型下的订阅者函数
class Shop {
constructor() {
// 存储订阅者的回调函数, 使用对象进行存储, 区分不同类型的订阅 { 'moreThan': [fn, fn2] }
this.subs = {}
}
// 订阅
on(type, fn) {
// 如果之前没有该类型的订阅,新加一个
if (!this.subs[type]) {
this.subs[type] = []
}
// 添加订阅者的函数
this.subs[type].push(fn)
}
// 发布信息
emit() {
// 取出类型, 类型在arguments对象的第一个, 取出第一个后, 后面的就是真正的参数
const type = [].shift.call(arguments)
// 依次调用该类型下的所有函数
const fnList = this.subs[type]
if(fnList && fnList.length > 0) {
// 取出订阅者,发出通知
fnList.forEach(sub => {
sub.apply(this, arguments)
})
}
}
}
使用如下:
const shop = new Shop()
// 订阅者订阅消息
shop.on('atDayAndLessPrice', (day, price) => {
console.log(`您期待天到货${day}, 期待价格是${price}`)
})
shop.on('moreThan', (num) => {
console.log(`您期数量大于${num}`)
})
setTimeout(() => {
shop.emit('atDayAndLessPrice', 30, 998)
shop.emit('moreThan', 20)
shop.emit('moreThan', 30)
}, 3000)
结果如下:
您期待天到货30, 期待价格是998
您期数量大于20
移除订阅
上面的功能只有添加, 没有移除, 其实说白了,就是把订阅者的回调函数移除就行了,当订阅函数为空时,直接移除掉该类型的订阅
注意, 需要比对是不是同一个函数,所以订阅的时候不能使用匿名函数, 只能先定义回调函数,在订阅消息
- 添加
remove方法
- 获取需要移除的订阅类型
- 通过类型,获取该类型下的所有函数
- 找到需要移除的函数,进行移除
- 判断该类型下, 回调函数列表是否为空,如果为空,直接移除该类型
// 定义Shop类
class Shop {
constructor() {
// 存储订阅者, 使用对象进行存储, 区分不同类型的订阅 { 'moreThan': [fn, fn2] }
this.subs = {}
}
// 省略...
// 移除订阅
remove(type, fn) {
const fnList = this.subs[type]
if(fnList && fnList.length > 0) {
// 查找需要移除函数的索引
const index = fnList.findIndex(v => v === fn)
if(index !== -1) {
// 移除
this.subs[type].splice(index, 1)
}
// 该类型下回调函数全部被移除, 去掉该类型
if(fnList.length === 0) {
this.subs[type] = null
delete this.subs[type]
}
}
}
}
使用示例:
需要定义具名函数,方便移除,否则不知道是移除那个函数
const shop = new Shop()
const dayPriceTest = (day, price) => {
console.log(`您期待天到货${day}, 期待价格是${price}`)
}
const numTest = (num) => {
console.log(`您期数量大于${num}`)
}
const numTest2 = (num) => {
console.log(`那啥, 您期数量真的真的大于: ${num}`)
}
// 订阅者订阅消息
shop.on('atDayAndLessPrice', dayPriceTest)
shop.on('moreThan', numTest)
shop.on('moreThan', numTest2)
// shop.remove('atDayAndLessPrice', dayPriceTest)
setTimeout(() => {
shop.emit('atDayAndLessPrice', 30, 998)
shop.emit('moreThan', 20)
}, 3000)
结果如下:
您期待天到货30, 期待价格是998
03 发布-订阅.html:112 那啥, 您期数量真的真的大于: 20