發(fā)布者-訂閱者模式簡單實現

之前在看DMQ根據vue雙向數據綁定原理模擬實現了mvvm,里面有提高發(fā)布者-訂閱者模式,看了一些資料,今天自己簡單實現了一個發(fā)布-訂閱模式。

何為發(fā)布-訂閱模式?

其定義對象間一種一對多的依賴關系,當一個對象的狀態(tài)發(fā)生改變時,所有依賴于它的對象都將得到通知。

作了一幅畫,關于兩者的關系說明:


發(fā)布-訂閱模式圖解.png

首次接觸這個概念的時候,會有幾個疑問,對象?指DOM對象還是自定義對象,還是兩者均可?依賴如何建立的?一個對象狀態(tài)的改變如何影響所有依賴它的對象?
這里面以微信公眾號為例,展開說明:

  • 假如用戶A訂閱了 某一個公眾號G,那么當公眾號G推送消息的時候,用戶A就會收到相關的推送,點開可以查看推送的消息內容。
  • 但是公眾號G并不關心訂閱的它的是男人、女人還是二哈,它只負責發(fā)布自己的主體,只要是訂閱公眾號的用戶均會收到該消息。
  • 作為用戶A,不需要時刻打開手機查看公眾號G是否有推動消息,因為在公眾號推送消息的那一刻,用戶A就會收到相關推送。
  • 當然了,用戶A如果不想繼續(xù)關注公眾號G,那么可以取消關注,取關以后,公眾號G再推送消息,A就無法收到了。
發(fā)布-訂閱模式抽象化

上面即是對發(fā)布-訂閱實例化的描述,但是跟上面問題的答案還是有些差距,我們付諸于代碼,以代碼的形式來模擬訂閱消息、發(fā)布消息、取消訂閱的功能,來解決上面提到的問題:

        // 01-定義一個訂閱-發(fā)布模式函數;
        function Pub2Sub() {
            // 02-訂閱器;
            this._observer = {}
        }
        // 03-原型對象上面添加方法;
        Pub2Sub.prototype = {
            constructor: Pub2Sub,
            // 04-訂閱者;
            subscribe: function (type, callback) {
                if (Object.prototype.toString.call(callback) !== '[object Function]') return
                // 訂閱器中是否存在訂閱行為;
                if (!this._observer[type]) this._observer[type] = []
                this._observer[type].push(callback)
                return this
            },
            // 05-發(fā)布者;
            publish: function () {
                let _self = this
                // 獲取發(fā)布行為
                let type = Array.prototype.shift.call(arguments)
                // 獲取發(fā)布主題
                let theme = Array.prototype.slice.call(arguments)
                // 獲取相關主題所有訂閱者
                let subscribes = _self._observer[type]
                // 發(fā)布主題
                if (!subscribes || !subscribes.length) {
                    console.warn('unsubscribe action or no actions in observer, please check out')
                    return
                }
                subscribes.forEach(callback => {
                    callback.apply(_self, theme)
                })
                return _self
            },
            // 06-取消訂閱
            unsubscrible: function (type, callback) {
                if (!this._observer[type] || !this._observer[type].length) return
                let subscribes = this._observer[type]
                subscribes.some((item, index, arr) => {
                    if (item === callback) {
                      // 刪除對應的訂閱行為
                        arr.splice(index, 1)
                        return true
                    }
                })
                return this
            }
        }
        // 實例化發(fā)布-訂閱模式
        let ps = new Pub2Sub()

        // 添加訂閱
        let sub1 = function (data) {
            console.log('sub1' + data)
        }
        let sub2 = function (data) {
            console.log('sub2' + data)
        }
        ps.subscribe('click', sub1)
        ps.subscribe('click', sub2)

        // 實現發(fā)布、取訂及再發(fā)布
        ps.publish('click', '第一次點擊消息').unsubscrible('click', sub2).publish('click', '第二次點擊消息')
        // 打印結果依次是:
        // sub1第一次點擊消息
        // sub2第一次點擊消息
        // sub1第二次點擊消息

上面代碼塊中,訂閱者1 sub1 和 訂閱者 sub2 分別訂閱了 'click',這個行為,當發(fā)布者 ps.publish 發(fā)布主題的時候,sub1sub2 均收到了消息,在控制臺輸出 sub1第一次點擊消息sub2第一次點擊消息,然后 訂閱者 sub2 又取訂了 click 行為,所以當 發(fā)布者 ps.publish 再次發(fā)布主題的時候,只有 sub1 才收到相關消息。
那么我們就通過代碼闡述了依賴是如何建立的,就是通過訂閱器來實現;

但是,上述實現的代碼存在兩個問題:

  • 訂閱行為需要在發(fā)布行為之前,如果直接發(fā)布主題,訂閱器中沒有相關的訂閱行為,我這里手動拋出了警告。但是這是不應該的,正如用戶A訂閱了公眾號G,也可以查看G的歷史消息,所以這里需要實現查看發(fā)布主題歷史記錄的功能;
  • 其次,上述功能的實現是通過定義在一個自定義對象,這樣就與發(fā)布-訂閱模式的松散耦合理念有些出入,所以還需要做到如何更優(yōu)雅的管理接口。
發(fā)布-訂閱模式優(yōu)化版

針對上述的問題,我在這個版本里面做了優(yōu)化,看代碼:

// 聲明一個全局發(fā)布-訂閱對象,為不同模塊之間的可能存在的通信做鋪墊
const Observer = (function () {
            // 訂閱器
            const _observer = {}
            // 歷史記錄
            const _cache = {},
                _shift = Array.prototype.shift,
                _slice = Array.prototype.slice,
                _toString = Object.prototype.toString
            // 訂閱
            const subscribe = function (type, callback) {
                if (_toString.call(callback) !== '[object Function]') return
                // 訂閱器中是否存在訂閱行為;
                if (!_observer[type]) _observer[type] = []
                _observer[type].push(callback)
                return this
            }
            // 發(fā)布
            const publish = function () {
                // 獲取發(fā)布行為
                let type = _shift.call(arguments)
                // 獲取發(fā)布主題
                let theme = _slice.call(arguments)
                // 記錄發(fā)布主題
                if (!_cache[type]) {
                    _cache[type] = [theme]
                } else {
                    _cache[type].push(theme)
                }
                // 獲取相關主題所有訂閱者行為
                let subscribes = _observer[type]
                // 發(fā)布主題
                if (!subscribes || !subscribes.length) return
                subscribes.forEach(callback => {
                    callback.apply(this, theme)
                })
                return this
            }
            // 取訂
            const unsubscrible = function (type, callback) {
                if (!_observer[type] || !_observer[type].length) return
                let subscribes = _observer[type]
                subscribes.some((item, index, arr) => {
                    if (item === callback) {
                        arr.splice(index, 1)
                        return true
                    }
                })
                return this
            }
            // 查看發(fā)布記錄
            const viewLog = function (type, callback) {
                if (!_cache[type] || _toString.call(callback) !== '[object Function]') return
                _cache[type].forEach(item => {
                    callback.apply(this, item)
                })
                return this
            }
            return {
                _observer,
                _cache,
                subscribe,
                publish,
                unsubscrible,
                viewLog
            }
        }())
        // 先發(fā)布主題;
        Observer.publish('click', '第一次發(fā)布點擊消息')
        Observer.publish('focus', '第一次發(fā)布聚焦消息')
        Observer.publish('blur', '第一次發(fā)布失焦消息')

        // 訂閱
        let sub1 = function (data) {
            console.log('sub1' + data)
        }
        let sub2 = function (data) {
            console.log('sub2' + data)
        }
        let sub3 = function (data) {
            console.log('sub3' + data)
        }
        Observer.subscribe('click', sub1)
        Observer.subscribe('click', sub2)
        Observer.subscribe('focus', sub3)

        // 再發(fā)布、取訂、查看發(fā)布記錄
        Observer.publish('click', '第二次發(fā)布點擊消息').unsubscrible('click', sub2).publish('click', '第三次發(fā)布點擊消息').publish('focus', '第二次發(fā)布聚焦消息').viewLog('click', function (message) {
                console.log(message)
            })

我們現在無論是先發(fā)布主題再訂閱,還是訂閱之后再發(fā)布主題,都不會有問題,因為在 Observer.publish 里面,發(fā)布者只關注自己發(fā)布主題功能,并且發(fā)布的時候將自己發(fā)布的對應主題保存。
在發(fā)布功能里面添加一個存放發(fā)布記錄的功能,在這里面我存放的是一個數組,是為了在 Observer.viewLog() 中方便調用。
通過一系列的發(fā)布、取訂、再發(fā)布、以及查看發(fā)布記錄,打印結果如下:

sub1第二次發(fā)布點擊消息
sub2第二次發(fā)布點擊消息
sub1第三次發(fā)布點擊消息
sub3第二次發(fā)布聚焦消息
// 這是查看歷史發(fā)布主題的結果,因為針對 click 行為,一共發(fā)布了三次主題
第一次發(fā)布點擊消息
第二次發(fā)布點擊消息
第三次發(fā)布點擊消息
理解對象間一對多的依賴關系

回到最初我們的問題,這個對象指的是既可以是自定義對象也可以是DOM對象

  • 定義兩個模塊
  let moduleA = {
          // 偽代碼
          todo() {
            Observer.subscribe(type1, function (data) {
                // 拿到 data 然后做一些事情
            })
        }
    }
  let moduleB = {
          // 偽代碼
          todo() {
            Observer.subscribe(type1, function (data) {
                // 拿到 data 然后做一些事情
            })
        }
    }
  // 下面是異步獲取到數據
 // 偽代碼
  ajax(function (data) {
        // 發(fā)布數據,所有的訂閱均會拿到 data,然后按照自己的邏輯處理
        Observer.publish(type, data)
    })

可能會有人疑問,為什么需要這樣來傳遞數據,直接在 moduleAmoduleB 里面直接獲取數據不可以嗎?
答案肯定是可以的,但是發(fā)布-訂閱這種模式可以更優(yōu)雅地在不同模塊之間傳遞數據。

2019/02/09
const isFun = function (fun) {
  return typeof fun === 'function'
}
class Observer {
  constructor () {
    this.messageCollector = {}
    this.history = {}
  }
  on (...arg) {
    const [type, callback] = arg
    if (!isFun(callback)) {
      throw new TypeError(`callback of arguments for function ${this.subscribe.name} must be a function `)
    }
    if (!this.messageCollector[type]) this.messageCollector[type] = []
    this.messageCollector[type].push(callback)
    return this
  }
  emit (...arg) {
    const [type, ...theme] = arg
    const subscribes = this.messageCollector[type]
    if (!this.history[type]) {
      this.history[type] = [theme]
    } else {
      this.history[type].push(theme)
    }
    for (const callback of subscribes) {
      callback.apply(this, theme)
    }
    return this
  }
  off (...arg) {
    const [type, callback] = arg
    if (!this.messageCollector[type] || !this.messageCollector[type].length) return
    if (!isFun(callback)) {
      throw new TypeError(`callback of arguments for function ${this.subscribe.name} must be a function `)
    }
    const subscribes = this.messageCollector[type]
    subscribes.some((item, index, arr) => {
      if (item === callback) {
        arr.splice(index, 1)
        return true
      }
    })
    return this
  }
  viewLog (...arg) {
    const [type, callback] = arg
    if (!this.history[type] || !isFun(callback)) return
    const themes = this.history[type]
    for (const theme of themes) {
      callback.apply(this, theme)
    }
    return this
  }
  reset () {
    this.messageCollector = {}
    this.history = {}
    return this
  }
}
寫在最后
  • 有人將觀察者模式和發(fā)布-訂閱模式認為是同一種模式,也有認為不是一種,仁者見仁,這里貼出一篇博客對兩者的介紹: 觀察者模式與發(fā)布/訂閱模式區(qū)別;
  • 關于本人實現的發(fā)布-訂閱模式,仍存在問題,如果訂閱行為過多,在團隊協(xié)作中,會面臨著命名沖突的局面,我就拋磚引玉,貼出大牛對這塊邏輯的處理:JavaScript設計模式--觀察者模式
  • 最后再貼出DMQ對vue響應式原理的實現過程:mvvm,如果想深入了解vue原理,是一個不錯的過渡選擇。
  • 關于發(fā)布-訂閱模式,在 ES6 里面有了更好的實現,下次有時間的時候再繼續(xù)分享。
  • 本文為原創(chuàng)文章,如果需要轉載,請注明出處,方便溯源,如有錯誤地方,可以在下方留言,歡迎??保创a已上傳到我的GitHub。
最后編輯于
?著作權歸作者所有,轉載或內容合作請聯系作者
【社區(qū)內容提示】社區(qū)部分內容疑似由AI輔助生成,瀏覽時請結合常識與多方信息審慎甄別。
平臺聲明:文章內容(如有圖片或視頻亦包括在內)由作者上傳并發(fā)布,文章內容僅代表作者本人觀點,簡書系信息發(fā)布平臺,僅提供信息存儲服務。

相關閱讀更多精彩內容

友情鏈接更多精彩內容