發(fā)布者-訂閱者模式簡(jiǎn)單實(shí)現(xiàn)

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

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

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

作了一幅畫,關(guān)于兩者的關(guān)系說明:


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

首次接觸這個(gè)概念的時(shí)候,會(huì)有幾個(gè)疑問,對(duì)象?指DOM對(duì)象還是自定義對(duì)象,還是兩者均可?依賴如何建立的?一個(gè)對(duì)象狀態(tài)的改變?nèi)绾斡绊懰幸蕾囁膶?duì)象?
這里面以微信公眾號(hào)為例,展開說明:

  • 假如用戶A訂閱了 某一個(gè)公眾號(hào)G,那么當(dāng)公眾號(hào)G推送消息的時(shí)候,用戶A就會(huì)收到相關(guān)的推送,點(diǎn)開可以查看推送的消息內(nèi)容。
  • 但是公眾號(hào)G并不關(guān)心訂閱的它的是男人、女人還是二哈,它只負(fù)責(zé)發(fā)布自己的主體,只要是訂閱公眾號(hào)的用戶均會(huì)收到該消息。
  • 作為用戶A,不需要時(shí)刻打開手機(jī)查看公眾號(hào)G是否有推動(dòng)消息,因?yàn)樵诠娞?hào)推送消息的那一刻,用戶A就會(huì)收到相關(guān)推送。
  • 當(dāng)然了,用戶A如果不想繼續(xù)關(guān)注公眾號(hào)G,那么可以取消關(guān)注,取關(guān)以后,公眾號(hào)G再推送消息,A就無(wú)法收到了。
發(fā)布-訂閱模式抽象化

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

        // 01-定義一個(gè)訂閱-發(fā)布模式函數(shù);
        function Pub2Sub() {
            // 02-訂閱器;
            this._observer = {}
        }
        // 03-原型對(duì)象上面添加方法;
        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)
                // 獲取相關(guān)主題所有訂閱者
                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) {
                      // 刪除對(duì)應(yīng)的訂閱行為
                        arr.splice(index, 1)
                        return true
                    }
                })
                return this
            }
        }
        // 實(shí)例化發(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)

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

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

但是,上述實(shí)現(xiàn)的代碼存在兩個(gè)問題:

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

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

// 聲明一個(gè)全局發(fā)布-訂閱對(duì)象,為不同模塊之間的可能存在的通信做鋪墊
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)
                }
                // 獲取相關(guān)主題所有訂閱者行為
                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ā)布點(diǎn)擊消息')
        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ā)布點(diǎn)擊消息').unsubscrible('click', sub2).publish('click', '第三次發(fā)布點(diǎn)擊消息').publish('focus', '第二次發(fā)布聚焦消息').viewLog('click', function (message) {
                console.log(message)
            })

我們現(xiàn)在無(wú)論是先發(fā)布主題再訂閱,還是訂閱之后再發(fā)布主題,都不會(huì)有問題,因?yàn)樵?Observer.publish 里面,發(fā)布者只關(guān)注自己發(fā)布主題功能,并且發(fā)布的時(shí)候?qū)⒆约喊l(fā)布的對(duì)應(yīng)主題保存。
在發(fā)布功能里面添加一個(gè)存放發(fā)布記錄的功能,在這里面我存放的是一個(gè)數(shù)組,是為了在 Observer.viewLog() 中方便調(diào)用。
通過一系列的發(fā)布、取訂、再發(fā)布、以及查看發(fā)布記錄,打印結(jié)果如下:

sub1第二次發(fā)布點(diǎn)擊消息
sub2第二次發(fā)布點(diǎn)擊消息
sub1第三次發(fā)布點(diǎn)擊消息
sub3第二次發(fā)布聚焦消息
// 這是查看歷史發(fā)布主題的結(jié)果,因?yàn)獒槍?duì) click 行為,一共發(fā)布了三次主題
第一次發(fā)布點(diǎn)擊消息
第二次發(fā)布點(diǎn)擊消息
第三次發(fā)布點(diǎn)擊消息
理解對(duì)象間一對(duì)多的依賴關(guān)系

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

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

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

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ā)布-訂閱模式認(rèn)為是同一種模式,也有認(rèn)為不是一種,仁者見仁,這里貼出一篇博客對(duì)兩者的介紹: 觀察者模式與發(fā)布/訂閱模式區(qū)別;
  • 關(guān)于本人實(shí)現(xiàn)的發(fā)布-訂閱模式,仍存在問題,如果訂閱行為過多,在團(tuán)隊(duì)協(xié)作中,會(huì)面臨著命名沖突的局面,我就拋磚引玉,貼出大牛對(duì)這塊邏輯的處理:JavaScript設(shè)計(jì)模式--觀察者模式
  • 最后再貼出DMQ對(duì)vue響應(yīng)式原理的實(shí)現(xiàn)過程:mvvm,如果想深入了解vue原理,是一個(gè)不錯(cuò)的過渡選擇。
  • 關(guān)于發(fā)布-訂閱模式,在 ES6 里面有了更好的實(shí)現(xiàn),下次有時(shí)間的時(shí)候再繼續(xù)分享。
  • 本文為原創(chuàng)文章,如果需要轉(zhuǎn)載,請(qǐng)注明出處,方便溯源,如有錯(cuò)誤地方,可以在下方留言,歡迎校勘,源碼已上傳到我的GitHub。
最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
【社區(qū)內(nèi)容提示】社區(qū)部分內(nèi)容疑似由AI輔助生成,瀏覽時(shí)請(qǐng)結(jié)合常識(shí)與多方信息審慎甄別。
平臺(tái)聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點(diǎn),簡(jiǎn)書系信息發(fā)布平臺(tái),僅提供信息存儲(chǔ)服務(wù)。

相關(guān)閱讀更多精彩內(nèi)容

友情鏈接更多精彩內(nèi)容