看到一篇介绍关于观察者模式和订阅发布模式的区别的文章,看完后依然认为它们在概念和思想上是统一的,只是根据实现方式和使用场景的不一样,叫法不同,不过既然有区别,就来探究一番,加深理解。javascript
先看图感觉下二者表现出来的区别:html
观察者模式的定义是在对象之间定义一个一对多的依赖,当对象自身状态改变的时候,会自动通知给关心该状态的观察者。前端
解决了主体对象与观察者之间功能的耦合,即一个对象状态改变给其余对象通知的问题。vue
这种对象与对象,有点像 商家-顾客 的关系,顾客对商家的某个商品感兴趣,就被商家记住,等有新品发布,便会直接通知顾客,相信加过微商微信会深有体会。java
来张图直观感觉:node
能够从图中看出来,这种模式是商家直接管理顾客。web
订阅发布模式微信
该模式理解起来和观察者模式同样,也是定义一对多的依赖关系,对象状态改变后,通知给全部关心这个状态的订阅者。函数
订阅发布模式有订阅的动做,能够不和商家直接产生联系,只要能订阅上关心的状态便可,一般利用第三方媒介来作,而发布者也会利用三方媒介来通知订阅者。ui
这有点像 商家-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 中会经过拦截数据的获取进行依赖收集,收集的是一个个 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 方法。
总结
经过上述的表述,相信你对观察者模式和订阅发布模式有了从新的认识,能够说两者是相同的,它们的概念和解决的问题是同样的,致力于让两个对象解耦,只是叫法不同;也能够说两者不同,在使用方式和场景中不同。
若是对你有帮助,请关注【前端技能解锁】: