觀察者模式和訂閱發佈模式是同樣的嗎?

觀察者模式和訂閱發佈模式是同樣的嗎?

看到一篇介紹關於觀察者模式和訂閱發佈模式的區別的文章,看完後依然認爲它們在概念和思想上是統一的,只是根據實現方式和使用場景的不一樣,叫法不同,不過既然有區別,就來探究一番,加深理解。javascript

先看圖感覺下二者表現出來的區別:html

watcher.png

兩種模型概念

觀察者模式的定義是在對象之間定義一個一對多的依賴,當對象自身狀態改變的時候,會自動通知給關心該狀態的觀察者。前端

解決了主體對象與觀察者之間功能的耦合,即一個對象狀態改變給其餘對象通知的問題。vue

這種對象與對象,有點像 商家-顧客 的關係,顧客對商家的某個商品感興趣,就被商家記住,等有新品發佈,便會直接通知顧客,相信加過微商微信會深有體會。java

來張圖直觀感覺:node

watcher2.png

能夠從圖中看出來,這種模式是商家直接管理顧客。web

訂閱發佈模式微信

該模式理解起來和觀察者模式同樣,也是定義一對多的依賴關係,對象狀態改變後,通知給全部關心這個狀態的訂閱者。函數

訂閱發佈模式有訂閱的動做,能夠不和商家直接產生聯繫,只要能訂閱上關心的狀態便可,一般利用第三方媒介來作,而發佈者也會利用三方媒介來通知訂閱者。ui

這有點像 商家-APP-顧客 的關係,某個產品斷貨,顧客能夠在APP上訂閱上貨通知,待上新,商家經過APP通知訂閱的顧客。

在程序實現中,第三方媒介稱之爲 EventBus(事件總線),能夠理解爲訂閱事件的集合,它提供訂閱、發佈、取消等功能。訂閱者訂閱事件,和發佈者發佈事件,都經過事件總線進行交互。

eventBus.png

兩種模式的異同

從概念上理解,二者沒什麼不一樣,都在解決對象之間解耦,經過事件的方式在某個時間點進行觸發,監聽這個事件的訂閱者能夠進行相應的操做。

在實現上有所不一樣,觀察者模式對訂閱事件的訂閱者經過發佈者自身來維護,後續的一些列操做都要經過發佈者完成;訂閱發佈模式是訂閱者和發佈者中間會有一個事件總線,操做都要通過事件總線完成。

觀察者模式的事件名稱,一般由發佈者指定發佈的事件,固然也能夠自定義,這樣看是否提供自定義的功能。

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 進行組件更新。能夠經過一張圖看到這個收集和通知過程。

vue.png

這些依賴存在了定義的 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 方法。

總結

經過上述的表述,相信你對觀察者模式和訂閱發佈模式有了從新的認識,能夠說兩者是相同的,它們的概念和解決的問題是同樣的,致力於讓兩個對象解耦,只是叫法不同;也能夠說兩者不同,在使用方式和場景中不同。

若是對你有幫助,請關注【前端技能解鎖】:

相關文章
相關標籤/搜索