看到一篇介紹關於觀察者模式和訂閱發佈模式的區別的文章,看完後依然認爲它們在概念和思想上是統一的,只是根據實現方式和使用場景的不一樣,叫法不同,不過既然有區別,就來探究一番,加深理解。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 方法。
總結
經過上述的表述,相信你對觀察者模式和訂閱發佈模式有了從新的認識,能夠說兩者是相同的,它們的概念和解決的問題是同樣的,致力於讓兩個對象解耦,只是叫法不同;也能夠說兩者不同,在使用方式和場景中不同。
若是對你有幫助,請關注【前端技能解鎖】: