目录
- 订阅发布模式是什么
- 图解发布订阅模式
- Vue中的应用 – 子组件向父组件传值
- 手写一个简易 EventBus
- 总结
- 发布订阅模式的优点与缺点
一 订阅发布模式是什么
发布订阅模式指的是希望接收通知的对象(Subscriber)基于一个主题通过自定义事件订阅主题,发布事件的对象(Publisher)通过发布主题事件的方式通知各个订阅该主题的 Subscriber 对象。
在发布-订阅模式,消息的发送方,叫做发布者(publishers) ,消息不会直接发送给特定的接收者,叫做订阅者。
意思就是发布者和订阅者不知道对方的存在。需要一个第三方组件,叫做信息中介,它将订阅者和发布者串联起来,它过滤和分配所有输入的消息。换句话说,发布-订阅模式用来处理不同系统组件的信息交流,即使这些组件不知道对方的存在。
那么如何过滤消息的呢?事实上这里有几个过程,最流行的方法是:基于主题以及基于内容。好了,就此打住,如果你感兴趣,可以去维基百科了解。
二 图解发布订阅模式 与 观察者模式
1) 图解发布订阅模式
发布-订阅模式(图片来源: MSDN 博客)
2) 观察者模式
3) 图解发布订阅模式 VS 观察者模式
图片来源: developers-club
三 Vue中的应用 – 子组件向父组件传值
Vue
开发中大家时常会写到的一种「发布订阅」模式:
<Child @submit="sendPost"></Child>
复制代码
相信写过 Vue
的同学都不陌生,这是组件间的方法传值,一点子组件内通过 emit
方法发布 submit
,父组件的 sendPost
方法就会被触发。
1) 子父通信demo
修改 Home.vue
如下所示:
<template>
<div class="home">
技能:{{ skill }}
<Child />
</div>
</template>
<script>
import Child from '@/components/Child'
import eventBus from '@/utils/event_bus'
import { onMounted, ref } from 'vue'
export default {
name: 'Home',
components: {
Child
},
setup() {
const skill = ref('')
onMounted(() => {
// 订阅 skill 类型名
eventBus.on('skill', (key) => {
skill.value = key
console.log('key', key)
})
})
return {
skill
}
}
}
</script>
复制代码
子组件 Child.vue
,如下所示:
<template>
<div>
<button @click="play">释放子技能</button>
<Grandson />
</div>
</template>
<script>
import eventBus from '@/utils/event_bus'
export default {
name: 'Child',
setup() {
const play = () => {
// 发布 skill 类型方法,并且传参数
eventBus.emit('skill', '狮子歌歌')
}
return {
play
}
}
}
</script>
复制代码
四 手写一个简易 EventBus
简单描述一下需求,EventBus 类中抛出 3 个方法,分别是:
- on:订阅方法,在某个组件或者页面引入 on 方法,定义触发的函数方法。
- emit:触发方法,根据上面的订阅方法,触发它。
- off:销毁订阅的类型,类似
document.removeEventListener
。
class EventBus {
constructor() {
this.handleMaps = {} // 初始化一个存放订阅回调方法的执行栈
}
// 订阅方法,接收两个参数
// type: 类型名称
// handler:订阅待执行的方法
on(type, handler) {
if (!(handler instanceof Function)) {
throw new Error('别闹了,给函数类型') // handler 必须是可执行的函数
}
// 如果类型名不存在,则新建对应类型名的数组
if (!(type in this.handleMaps)) {
this.handleMaps[type] = []
}
// 将待执行方法塞入对应类型名数组
this.handleMaps[type].push(handler)
}
// 发布方法,接收两个参数
// type:类型名称
// params:传入待执行方法的参数
emit(type, params) {
if (type in this.handleMaps) {
this.handleMaps[type].forEach(handler => {
// 执行订阅时,塞入的待执行方法,并且带入 params 参数
handler(params)
})
}
}
// 销毁方法
off(type) {
if (type in this.handleMaps) {
delete this.handleMap[type]
}
}
}
export default new EventBus()
复制代码
参考
总结
-
市面上的状态管理插件,无论是
Vuex
、Redux
、Mobx
等,都用到了「发布订阅」模式。它的设计思路值得我们去深思和探索,上述手写的建议EventBus
是通用的,无论是 Vue、React、Angular 或者是原生项目,都适用。 -
发布订阅模式指的是希望接收通知的对象(Subscriber)基于一个主题通过自定义事件订阅主题,发布事件的对象(Publisher)通过发布主题事件的方式通知各个订阅该主题的 Subscriber 对象。
观察者模式
:
观察者(Observer)直接订阅(Subscribe)主题(Subject),而当主题被激活的时候,会触发(Fire Event)观察者里的事件。
观察者实例对象 是被
目标对象添加到
观察者列表`当中的,然后, 目标对象在合适的时机调用Notify去遍历调用所有的 观察者实例对象上的传入的 观察者
发布订阅模式
:订阅者(Subscriber)把自己想订阅的事件注册(Subscribe)到调度中心(Event Channel),当发布者(Publisher)发布该事件(Publish Event)到调度中心,也就是该事件触发时,由调度中心统一调度(Fire Event)订阅者注册到调度中心的处理代码。
差异
:
- 在观察者模式中,观察者是知道Subject的,Subject一直保持对观察者进行记
(目标对象是要将观察者add到 目标对象的观察者列表当中的)
。然而,在发布订阅模式中,发布者和订阅者不知道对方的存在。它们只有通过消息代理进行通信。 - 在发布订阅模式中,组件是松散耦合的,正好和观察者模式相反。
- 观察者模式大多数时候是同步的,比如当事件触发,Subject就会去调用观察者的方法。而发布-订阅模式大多数时候是异步的(使用消息队列)。
- 观察者 模式需要在单个应用程序地址空间中实现,而发布-订阅更像交叉应用模式。
尽管它们之间有区别,但有些人可能会说发布-订阅模式是观察者模式的变异,因为它们概念上是相似的。
六 发布订阅模式的优点与缺点
优点:
- 对象之间的解耦
- 异步编程中,可以更松耦合的代码编写
缺点:
- 创建订阅者本身要消耗一定的时间和内存
- 多个发布者和订阅者嵌套一起的时候,程序难以跟踪维护
七、发布订阅模式简单实现
1) 实现一
class EventBus{
constructor(){
// 在event对象中 存放 所有的事件与回调数组,如:
// {a: [ ()=>{console.log(1)}, (a)=>{console.log(a)}], b: [()=>{console.log(1)}]}
this.event=Object.create(null);
};
/**注册事件
* @输入 事件名 name , 对应的回调函数。
* @输出 undefined
* @功能 往event对象中加入事件与回调数组。
*/
on(name,fn){
if(!this.event[name]){
//一个事件可能有多个监听者
this.event[name]=[];
};
this.event[name].push(fn);
};
/**触发事件
* @输入 事件名 name , 剩余参数(触发的事件名对应的回调数组中,回调函数的参数(不定参))。
* @输出 undefined
* @功能 触发事件名对应的回调数组
*/
emit(name,...args){
//遍历要触发的事件对应的数组回调函数。依次调用数组当中的函数,并把参数传入每一个cb。
this.event[name] && this.event[name].forEach(fn => {
fn(...args)
});
};
/**只触发一次事件
* @输入 事件名 name ,回调函数fn
* @输出 undefined
* @功能 借助变量cb,同时完成了对该事件的注册、对该事件的触发,并在最后取消该事
*/
once(name,fn){
var cb=(...args)=>{
//触发
fn(...args);
//取消
this.off(name,fn);
};
//监听
this.on(name,cb);
};
/**取消事件
* @输入 事件名 name ,要取消的对应的回调
* @输出 undefined
* @功能 删除指定事件名,回调数组中的某一个回调。
*/
off(name,offcb){
if(this.event[name]){
// 找到要取消事件在回调数组中的索引
let index=this.event[name].findIndex((fn)=>{
return offcb===fn;
});
//通过索引删除掉对应回调数组中的回调函数。
this.event[name].splice(index,1);
// 回调数组长度为0时(没有回调数组时)
if(!this.event[name].length){
// 删除事件名
delete this.event[name];
}
}
}
}
复制代码
var a = new EventBus()
// on emit
a.on('a',()=>{console.log(1)});
a.on('a',(a)=>{console.log(a)});
a.on('b',()=>{console.log(1)});
a.emit('a','2'); // 1 2
a.emit('b'); // 1
//此时的event对象是
console.log(a.event);
// {a: [ ()=>{console.log(1)}, (a)=>{console.log(a)}], b: [()=>{console.log(1)}] }
// once
var fnc = (a)=>{console.log(a)};
a.once("d",fnc(1))
// off
var fnc = (a)=>{console.log(a)};
a.on('c',fnc);
a.event; // {a: Array(2), b: Array(1), c: Array(1), d: Array(2)}
a.off("c",fnc);
a.event; // {a: Array(2), b: Array(1), d: Array(2)}
复制代码
2) 实现2
const pubSub = {
list:{},
subscribe(key,fn){ // 订阅
if (!this.list[key]) {
this.list[key] = [];
}
this.list[key].push(fn);
},
publish(){ // 发布
const arg = arguments;
const key = Array.prototype.shift.call(arg);
const fns = this.list[key];
if(!fns || fns.length<=0) return false;
for(var i=0,len=fns.length;i<len;i++){
fns[i].apply(this, arg);
}
},
unSubscribe(key) { // 取消订阅
delete this.list[key];
}
};
// 进行订阅
pubSub.subscribe('name', (name) => {
console.log('your name is ' + name);
});
pubSub.subscribe('sex', (sex) => {
console.log('your sex is ' + sex);
});
// 进行发布
pubSub.publish('name', 'andy'); // your name is andy
pubSub.publish('sex', 'male'); // your sex is male
复制代码
上述代码的订阅是基于 name 和 sex 主题来自定义事件,发布是通过 name 和 sex 主题并传入自定义事件的参数,最终触发了特定主题的自定义事件。
可以通过 unSubscribe 方法取消特定主题的订阅。
pubSub.subscribe('name', (name) => {
console.log('your name is ' + name);
});
pubSub.subscribe('sex', (sex) => {
console.log('your sex is ' + sex);
});
pubSub.unSubscribe('name');
pubSub.publish('name', 'andy'); // 这个主题被取消订阅了
pubSub.publish('sex', 'male'); // your sex is male
复制代码
2.1) 图解订阅发布模式
2.2) 图解订阅流程
2.3 多次订阅 并发布 同一个主题
- 首先可以有多个主题, 每个主题都可以被订阅多次,发布多次
- 订阅之后,主题和对应的cb就会存到调度中心对象中;发布时,直接通过对应的主题名发布即可
/ 进行订阅
pubSub.subscribe('name', (name) => {
console.log('订阅 name 主题0 ' + name);
});
pubSub.subscribe('name', (name) => {
console.log('订阅 name 主题1 ' + name);
});
pubSub.subscribe('name', (name) => {
console.log('订阅 name 主题2 ' + name);
});
pubSub.subscribe('sex', (sex) => {
console.log('订阅 sex 主题 ' + sex);
});
// 进行发布
pubSub.publish('name', 'andy'); // your name is andy
pubSub.publish('sex', 'male'); // your sex is male
复制代码
再总结
观察者模式 VS 发布订阅模式
观察者模式与发布订阅模式都是定义了一个一对多的依赖关系,当有关状态发生变更时则执行相应的更新。
不同的是,在观察者模式中依赖于 Subject 对象的一系列 Observer 对象在被通知之后只能执行同一个特定的更新方法,而在发布订阅模式中则可以基于不同的主题去执行不同的自定义事件。相对而言,发布订阅模式比观察者模式要更加灵活多变。
我认为,观察者模式和发布订阅模式本质上的思想是一样的,而发布订阅模式可以被看作是观察者模式的一个进阶版。
设计模式只是一种思想,某一种设计模式都可以有很多种不同的实现方式,各种实现都有其优劣之分,具体的实现方式需要基于不同的业务场景
发布者和订阅者不知道对方的存在。需要一个第三方组件,叫做信息中介,它将订阅者和发布者串联起来,它过滤和分配所有输入的消息。换句话说,发布-订阅模式用来处理不同系统组件的信息交流,即使这些组件不知道对方的存在
两者都是一对多的关系,观察者模式是 被观察者要通知哪些观察者是需要被观察者添加这些观察者到自己的观察者列表当中的,就好比,Vue中 data中的属性a,是被观察者,(时机:业务场景:而当a改变的时候), 被观察者需要通知他的观察者列表去更新自己负责的DOM(也就是自己的cb)。而这个观察者列表是在(业务场景:get时收集的依赖观察者到观察者列表中),也就是 观察者们是被 被观察者 知道的,并且是被 被观察者自己添加到自己的观察者列表中的。 就比如
a 就是vue业务当中的被观察者(目标对象),每一个这样的目标对象都会有自己收集好的一个观察者列表。用于在特定的时机去遍历调用。
//
a={
value: 1,
_ob_:[观察者1,观察者2,...]
}
复制代码
【订阅发布模式】,也是一对多的关系,订阅主题,和发布主题之间有一个完全独立的调度中心。用来管理(主题事件和订阅者们)。发布哪个主题事件,以及何时发布,以及订阅者不知发布者的存在,**订阅者只知道去定于主题即可。而发布者也只管发布主题事件即可。 **
Vue中 子组件向父组件传递数据,通过订阅发布模式,父组件订阅子组件的主题事件,子组件去发布主题事件,并传递数据给父组件。业务目的: 子向父传递数据。