前言
《JavaScript设计模式与开发实践》一书中说分辨模式的关键是意图而不是结构。在意图方面上说,这两种模式的意图都是定义了对象间的一种一对多的依赖关系,当一个对象的状态发生改变时,所有依赖于它的对象都将得到通知,并自动更新;而从结构方面来说,观察者模式是观察者和被观察者(目标对象)之间的通讯,两者是直接关联的,而发布-订阅模式是发布者和订阅者之间的通讯,但是两者不是直接关联的。
一、观察者模式
观察者模式就是观察者和被观察者之间的通讯。描述的是对象间的一种一对多的关系,即一个或多个观察者对目标对象的状态感兴趣,通过将自己本身依附在目标对象上以便关注所感兴趣的内容。目标对象的状态发生改变,若观察者对这些改变感兴趣,会发送一个通知消息,调用每个观察者的更新方法,当观察者不再对目标状态感兴趣时,他们可以简单将自己从中分离。举个例子:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36
| class ObservedTarget { constructor() { this.observers = [] //用于存储所有的观察者 } addObserver(...observer) { // console.log(...observer); this.observers.push(...observer) //添加观察者 } notifyObserver(...args) { // 遍历观察者列表 this.observers.forEach(item => { item.update(...args) }) } } class Observer { constructor(name) { this.name = name } update(...args) { let content = [...args]; console.log(`${this.name}接收到目标对象更新的状态是:${content}`); } } // 创建多个观察者 let observer1 = new Observer('observer1') let observer2 = new Observer('observer2') let observer3 = new Observer('observer3') // 把观察者本身依附在目标对象上 let observerTarget = new ObservedTarget() observerTarget.addObserver(observer1, observer2, observer3)//直接关联 // 当目标对象更新内容时,通知所有的观察者 observerTarget.notifyObserver('这是我的新专辑!', '感谢粉丝对我的支持呀!') //observer1接收到目标对象更新的状态是:这是我的新专辑!,感谢粉丝对我的支持呀! //observer2接收到目标对象更新的状态是:这是我的新专辑!,感谢粉丝对我的支持呀! //observer3接收到目标对象更新的状态是:这是我的新专辑!,感谢粉丝对我的支持呀!
|
以上不难看出,多个观察者将自己本身依附在目标对象上,这样就可以做到直接相关联,当目标对象的状态发生改变时,通知消息(广播出去),那么所有的观察者就都可以得到最新的消息了。对于学习vue的胞友来说,这个vue中双向绑定原理就是观察者模式的应用之一,可以看看双向绑定的原理:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48
| let currentEffect = null;//在这里,通过这个全局变量将观察者和目标对象进行关联 class Dep {//就是目标对象 constructor(val) { this._val = val this.effects = new Set() //存储依赖,依赖只收集一次 } get value() { //get操作 // 读取 this.depend() //每次读取都会触发依赖收集 return this._val } set value(newVal) { //set操作 // 修改 this._val = newVal this.notify() //值更新完毕后,通知更新 return this._val } depend() { //收集依赖 // 收集依赖时,需要先将收集的依赖存储起来,而且不重复收集依赖 // 依赖是通过effectWatcher内部的回调函数配合effectWatcher实现的,所以需要关联到effectWatcher函数,可以先定义一个全局变量currentEffect if (currentEffect) { this.effects.add(currentEffect) } } notify() { //通知更新 // 遍历所有依赖并执行 this.effects.forEach(effect => { effect() }) } } // effect函数 function effectWatcher(effect) {//就是个观察者 currentEffect = effect //每收集一个依赖,都会关联到depend函数 effect() //保证一上来就执行 currentEffect = null } // 使用 const dep = new Dep('没有任何最新的动态~') let content; effectWatcher(() => { content = dep.value console.log(content); }) // 当值发生改变 dep.value = '目标对象发布新专辑了!' // 没有任何最新的动态~ // 目标对象发布新专辑了!
|
以上不难看出,Dep就充当目标对象,在vue中是依赖收集者;而就是effectWatcher观察者的身份了,在vue中是effect函数;其实这个vue的双向绑定原理的优化在于,我不再需要手动调用更新去广播,只要目标对象的值发生改变,会自动去广播。其实这就是观察者模式!
二、发布订阅模式
发布-订阅模式也是通过⼀对⼀或者⼀对多的依赖关 系,当对象发⽣改变时,订阅⽅都会收到通知。在现实⽣活中类似场景是相当的多,⽐如公众号的订阅,预售消息订阅等等。
但是,相比于观察者模式,发布订阅模式多了个事件通道,事件通道作为调度中心,管理事件的订阅和发布工作,彻底隔绝订阅者和发布者的依赖关系,直白地说,订阅者只是订阅自己感兴趣的内容,并不关心目标对象的存在;而目标对象也只是发布自己想发布的内容,并不关心订阅者的具体存在。发布者和订阅者并无直接联系。
再回到观察者模式,它有两个重要的角色,即目标和观察者。在目标和观察者之间是没有事件通道的。一方面,观察者要想订阅目标事件,由于没有事件通道,因此必须将自己添加(依附)到目标对象中进行管理;另一方面,目标在触发事件的时候,无法将通知操作委托给事件通道,而只能亲自去通知所有的观察者。
1. 例子
实际上,在生活中处处都有发布-订阅的存在,只要我们曾经在DOM节点上面绑定过事 件函数,那我们就曾经使用过发布—订阅模式,只是你不知道而已,举个例子:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17
| <div id="box"></div> <script> function click() { console.log(3); } let box = document.getElementById('box') box.addEventListener('click', () => { console.log(1); }) box.addEventListener('click', () => { console.log(2); }) box.addEventListener('click', () => { click() }) box.click()//模拟用户点击 </script>
|
当我点击box时,会同时打印1,2,3,如图所示:
2. 实现原理
其实,除去DOM事件,常用的还是自定义的其他事件,经过时间的洗礼,发布-订阅模式不再是简单的DOM事件可以体现的,现如今的发布-订阅模式除了发布、订阅,还有只订阅一次和取消订阅,在写发布-订阅模式之前呢,还需要知道一些关键的人物,下手才能行云流水,正所谓磨刀不误砍柴工:
- 确认发布者的职责,发布者需要一个缓存列表,用于存放所有的订阅者回调函数,以便消息通知;在发布时,发布者会遍历该缓存列表,依次触发存放的订阅者回调函数。
- 确认订阅者的需求,订阅者可以接收一些参数,这些参数是来自回调函数的,这些参数包括订阅者感兴趣内容相关的其他信息,当然,订阅者可以自行处理这些参数。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67
| class PubSubTopics {//调度中心 constructor() { this.subscribers = [] //用于存储所有的订阅者回调函数 } subscrible(message, callback) { //订阅 let callbacks = this.subscribers[message] if (!callbacks) { //不存在 this.subscribers[message] = [callback] } else { callbacks.push(callback) } } publish(message, ...args) { //发布 let callbacks = this.subscribers[message] || [] callbacks.forEach(callback => callback(...args)) } once(message, callback) { //只订阅一次 let onceFn = (...args) => { callback.apply(this, args) //当场执行,不进入缓存列表 this.remove(message) //再取消订阅 } this.subscrible(message, onceFn) } remove(message, callback) { //取消订阅 let callbacks = this.subscribers[message] || [] if (!callback) { //没有传入具体的回调函数,则取消对应的所有订阅 callbacks && (callbacks = []) } else { callbacks.forEach((cb, index) => { if (cb == callback) { //具名函数 callbacks.splice(index, 1)//删除 } }) } } } //测试 let subscriberA = new PubSubTopics() subscriberA.subscrible('song', (song) => { console.log('new song=', song); }) subscriberA.subscrible('teleplay', userA = (teleplay) => { console.log('new teleplay=', teleplay); }) subscriberA.subscrible('teleplay', userB = (teleplay) => { console.log('new teleplay=', teleplay); }) subscriberA.subscrible('teleplay', userC = (teleplay) => { console.log('new teleplay=', teleplay); }) subscriberA.subscrible('movie', (movie) => { console.log('new movie=', movie); }) // 取消订阅 subscriberA.remove('teleplay', userA) subscriberA.remove('teleplay', userC) // 只订阅一次 subscriberA.once('movie', (...args) => { console.log('我只订阅一次:', args); }) subscriberA.publish('song', '李荣浩 乌梅子酱') subscriberA.publish('teleplay', '张译 他是谁') subscriberA.publish('movie', '易烊千玺 满江红') //new song= 李荣浩 乌梅子酱 //new teleplay= 张译 他是谁 //new movie= 易烊千玺 满江红 //我只订阅一次: [ '易烊千玺 满江红' ]
|
至此,一个发布-订阅模式就大致实现了,其实在vue中,$on、$emit、$off、$once组合起来可不就是一个发布订阅模式了嘛。
三、总结
1. 观察者模式
观察者模式包括两个主体:观察者和被观察者(目标对象),它属于行为型模式(关注对象之间的通信),也是一种将代码解耦的设计模式,观察者不需要直接调用被观察者的内部方法或属性获取通知。
观察者模式描述的是对象之间一种一对一或一对多的关系,观察者需要将自己注册(依附)在被观察上。
2. 订阅发布模式
发布-订阅模式包括三个模块:发布者、订阅者和调度中心(办事大厅),发布者和订阅者两者之间是分离的,即订阅者只管在调度中心订阅,有人调用才响应;而发布者只管在调度中心广播。而且,在事件发生时,发布者一次性把所有更改的状态和数据都推送给订阅者。
发布-订阅模式的优点在于两个方面:一是时间上的解耦;而是对象之间的解耦。可以用在异步编程中,以便满足更加松耦合的需要。但是,万物都有两面性,发布订阅模式也不例外,该模式虽然可以弱化对象之间的耦合度,但是也不便于过渡使用,否则会难以跟踪和理解;另外,缓存大量的订阅者也需要消耗一定的时间和内存,一般是先订阅后发布,所以有些时候订阅者也许会永远存在内存中,但是这个消息可能始终未发生。
发布订阅模式和观察者模式相比多了一个事件的调度中心,而观察者模式是两者直接关联的。
vue的双向绑定原理就是观察者模式。