共计 5620 个字符,预计需要花费 15 分钟才能阅读完成。
观察者模式和订阅公布模式是一样的吗?
看到一篇介绍对于观察者模式和订阅公布模式的区别的文章,看完后仍然认为它们在概念和思维上是对立的,只是依据实现形式和应用场景的不同,叫法不一样,不过既然有区别,就来探索一番,加深了解。
先看图感触下两者体现进去的区别:
两种模型概念
观察者模式 的定义是在对象之间定义一个一对多的依赖,当对象本身状态扭转的时候,会主动告诉给关怀该状态的观察者。
解决了主体对象与观察者之间性能的耦合,即一个对象状态扭转给其余对象告诉的问题。
这种对象与对象,有点像 商家 - 顾客 的关系,顾客对商家的某个商品感兴趣,就被商家记住,等有新品公布,便会间接告诉顾客,置信加过微商微信会深有体会。
来张图直观感触:
能够从图中看进去,这种模式是商家间接治理顾客。
订阅公布模式
该模式了解起来和观察者模式一样,也是定义一对多的依赖关系,对象状态扭转后,告诉给所有关怀这个状态的订阅者。
订阅公布模式有订阅的动作,能够不和商家间接产生分割,只有能订阅上关怀的状态即可,通常利用第三方媒介来做,而发布者也会利用三方媒介来告诉订阅者。
这有点像 商家 -APP- 顾客 的关系,某个产品断货,顾客能够在 APP 上订阅上货告诉,待上新,商家通过 APP 告诉订阅的顾客。
在程序实现中,第三方媒介称之为 EventBus(事件总线),能够了解为订阅事件的汇合,它提供订阅、公布、勾销等性能。订阅者订阅事件,和发布者公布事件,都通过事件总线进行交互。
两种模式的异同
从概念上了解,两者没什么不同,都在解决对象之间解耦,通过事件的形式在某个工夫点进行触发,监听这个事件的订阅者能够进行相应的操作。
在实现上有所不同,观察者模式对订阅事件的订阅者通过发布者本身来保护,后续的一些列操作都要通过发布者实现;订阅公布模式是订阅者和发布者两头会有一个事件总线,操作都要通过事件总线实现。
观察者模式的事件名称,通常由发布者指定公布的事件,当然也能够自定义,这样看是否提供自定义的性能。
在 DOM 中绑定事件,click、mouseover 这些,都是内置规定好的事件名称。
document.addEventListener('click',()=>{})
addEventListener 第一个参数就是绑定的工夫名称;第二参数是一个函数,就是订阅者。
订阅公布模式的事件名称就比拟随便,在事件总线中会保护一个事件对应的订阅者列表,当该事件触发时,会遍历列表告诉所有的订阅者。
伪代码:
// 订阅 | |
EventBus.on('custom', () => {}) | |
// 公布 | |
EventBus.emit('custom') |
事件名称为开发者自定义,当应用频繁时保护起来较为麻烦,尤其是改名字,多个对象或组件都要替换,通常会把事件名称在一个配置中对立治理。
代码实现
观察者模式
在 Javascript 中函数就是对象,订阅者对象能够间接由函数来充当,就跟绑定 DOM 应用的 addEventListener 办法,第二个参数就是订阅者,是一个函数。
咱们从下面形容的概念中去实现 商家 - 顾客,这样能够更好的了解(或者迷糊)。
定义一个顾客类,须要有个办法,这个办法用来接管商家告诉的音讯,就跟顾客都留有手机号码一样,公布的音讯都由手机来接管,顾客收音讯的形式是对立的。
// 顾客 | |
class Customer {update(data){console.log('拿到了数据', data); | |
} | |
} |
定义商家,商家提供订阅、勾销订阅、公布性能
// 商家 | |
class Merchant {constructor(){this.listeners = {} | |
} | |
addListener(name, listener){ | |
// 事件没有,定义一个队列 | |
if(this.listeners[name] === undefined) {this.listeners[name] = []} | |
// 放在队列中 | |
this.listeners[name].push(listener) | |
} | |
removeListener(name, listener){ | |
// 事件没有队列,则不解决 | |
if(this.listeners[name] === undefined) return | |
// 遍历队列,找到要移除的函数 | |
const listeners = this.listeners[name] | |
for(let i = 0; i < listeners.length; i++){if(listeners[i] === listener){listeners.splice(i, 1) | |
i-- | |
} | |
} | |
} | |
notifyListener(name, data){ | |
// 事件没有队列,则不解决 | |
if(this.listeners[name] === undefined) return | |
// 遍历队列,顺次执行函数 | |
const listeners = this.listeners[name] | |
for(let i = 0; i < listeners.length; i++){if(typeof listeners[i] === 'object'){listeners[i].update(data) | |
} | |
} | |
} | |
} |
应用一下:
// 多名顾客 | |
const c1 = new Customer() | |
const c2 = new Customer() | |
const c3 = new Customer() | |
// 商家 | |
const m = new Merchant() | |
// 顾客订阅商家商品 | |
m.addListener('shoes', c1) | |
m.addListener('shoes', c2) | |
m.addListener('skirt', c3) | |
// 过了一天没来,勾销订阅 | |
setTimeout(() => {m.removeListener('shoes', c2) | |
}, 1000) | |
// 过了几天 | |
setTimeout(() => {m.notifyListener('shoes', '来啊,购买啊') | |
m.notifyListener('skirt', '提价了') | |
}, 2000) |
订阅公布模式
订阅和公布的性能都在事件总线中。
class Observe {constructor(){this.listeners = {} | |
} | |
on(name, fn){ | |
// 事件没有,定义一个队列 | |
if(this.listeners[name] === undefined) {this.listeners[name] = []} | |
// 放在队列中 | |
this.listeners[name].push(fn) | |
} | |
off(name, fn){ | |
// 事件没有队列,则不解决 | |
if(this.listeners[name] === undefined) return | |
// 遍历队列,找到要移除的函数 | |
const listeners = this.listeners[name] | |
for(let i = 0; i < this.listeners.length; i++){if(this.listeners[i] === fn){this.listeners.splice(i, 1) | |
i-- | |
} | |
} | |
} | |
emit(name, data){ | |
// 事件没有队列,则不解决 | |
if(this.listeners[name] === undefined) return | |
// 遍历队列,顺次执行函数 | |
const listenersEvent = this.listeners[name] | |
for(let i = 0; i < listenersEvent.length; i++){if(typeof listenersEvent[i] === 'function'){listenersEvent[i](data) | |
} | |
} | |
} | |
} |
应用:
const observe = new Observe() | |
// 进行订阅 | |
observe.on('say', (data) => {console.log('监听, 拿到数据', data); | |
}) | |
observe.on('say', (data) => {console.log('监听 2, 拿到数据', data); | |
}) | |
// 公布 | |
setTimeout(() => {observe.emit('say', '传过来数据啦') | |
}, 2000) |
通过以上两种模式的实现上来看,观察者模式进一步形象,能抽出公共代码就是事件总线,反过来说,如果一个对象要有观察者模式的性能,只须要继承事件总线。
node 中提供能了 events 模块可供咱们灵便应用。
继承应用,都通过发布者调用:
const EventEmitter = require('events') | |
class MyEmitter extends EventEmitter {} | |
const myEmitter = new MyEmitter() | |
myEmitter.on('event', (data) => {console.log('触发事件', data); | |
}); | |
myEmitter.emit('event', 1); |
间接应用,当做事件总线:
const EventEmitter = require('events') | |
const emitter = new EventEmitter() | |
emitter.on('custom', (data) => {console.log('接收数据', data); | |
}) | |
emitter.emit('custom', 2) |
利用场景
观察者模式 在很多场景中都在应用,除了上述中在 DOM 上监听事件外,还有最罕用的是 Vue 组件中父子之间的通信。
父级代码:
<template> | |
<div> | |
<h2> 父级 </h2> | |
<Child @custom="customHandler"></Child> | |
</div> | |
</template> | |
<script> | |
export default { | |
methods: {customHandler(data){console.log('拿到数据,我要干点事', data); | |
} | |
} | |
} | |
</script> |
子级代码:
<template> | |
<div> | |
<h2> 子级 </h2> | |
<button @click="clickHandler"> 扭转了 </button> | |
</div> | |
</template> | |
<script> | |
export default { | |
methods: {clickHandler(){this.$emit('custome', 123) | |
} | |
} | |
} | |
</script> |
子组件是一个通用的组件,外部不做业务逻辑解决,仅仅在点击时会公布一个自定义的事件 custom。子组件被应用在页面的任意中央,在不同的应用场景里,当点击按钮后子组件所在的场景会做相应的业务解决。如果关怀子组件外部按钮点击这个状态的扭转,只须要监听 custom 自定义事件。
订阅公布模式 在用 Vue 写业务也会应用到,利用场景是在跨多层组件通信时,如果利用父子组件通信一层层订阅公布,可维护性和灵活性很差,一旦两头某个环节出问题,整个流传链路就会瘫痪。这时采纳独立进去的 EventBus 解决这类问题,只有能拜访到 EventBus 对象,便可通过该对象订阅和公布事件。
// EventBus.js | |
import Vue from 'vue' | |
export default const EventBus = new Vue() |
父级代码:
<template> | |
<div> | |
<h2> 父级 </h2> | |
<Child></Child> | |
</div> | |
</template> | |
<script> | |
import EventBus from './EventBus' | |
export default { | |
// 加载完就要监控 | |
moutend(){EventBus.on('custom', (data) => {console.log('拿到数据', data); | |
}) | |
} | |
} | |
</script> |
<template> | |
<div> | |
<h2> 嵌套很深的子级 </h2> | |
<button @click="clickHandler"> 扭转了 </button> | |
</div> | |
</template> | |
<script> | |
import EventBus from './EventBus' | |
export default { | |
methods: {clickHandler(){EventBus.emit('custom', 123) | |
} | |
} | |
} | |
</script> |
通过上述代码能够看进去订阅公布模式齐全解耦两个组件,相互能够不晓得对方的存在,只须要在失当的机会订阅或公布自定义事件。
订阅公布模式在 Vue2 源码中的应用
Vue2 中会通过拦挡数据的获取进行依赖收集,收集的是一个个 Watcher。期待对数据进行变更时,要告诉依赖的 Watcher 进行组件更新。能够通过一张图看到这个收集和告诉过程。
这些依赖存在了定义的 Dep 中,在这个类中实现了简略的订阅和公布性能,能够看做是一个 EventBus,源码如下:
export default class Dep { | |
static target: ?Watcher; | |
id: number; | |
subs: Array<Watcher>; | |
constructor () { | |
this.id = uid++ | |
this.subs = []} | |
addSub (sub: Watcher) {this.subs.push(sub) | |
} | |
removeSub (sub: Watcher) {remove(this.subs, sub) | |
} | |
depend () {if (Dep.target) {Dep.target.addDep(this) | |
} | |
} | |
notify () { | |
// stabilize the subscriber list first | |
const subs = this.subs.slice() | |
for (let i = 0, l = subs.length; i < l; i++) {subs[i].update()} | |
} | |
} |
每个 Wather 就是订阅者,这些订阅者都实现一个叫做 update 的办法,当数据更改时便会遍历所有的 Wather 调用 update 办法。
总结
通过上述的表述,置信你对观察者模式和订阅公布模式有了从新的意识,能够说二者是雷同的,它们的概念和解决的问题是一样的,致力于让两个对象解耦,只是叫法不一样;也能够说二者不一样,在应用形式和场景中不一样。
如果对你有帮忙,请关注【前端技能解锁】: