美文网首页里程碑
发布者-订阅者模式简单实现

发布者-订阅者模式简单实现

作者: 一慢呀 | 来源:发表于2018-05-13 20:43 被阅读0次

    之前在看DMQ根据vue双向数据绑定原理模拟实现了mvvm,里面有提高发布者-订阅者模式,看了一些资料,今天自己简单实现了一个发布-订阅模式。

    何为发布-订阅模式?

    其定义对象间一种一对多的依赖关系,当一个对象的状态发生改变时,所有依赖于它的对象都将得到通知。

    作了一幅画,关于两者的关系说明:


    发布-订阅模式图解.png

    首次接触这个概念的时候,会有几个疑问,对象?指DOM对象还是自定义对象,还是两者均可?依赖如何建立的?一个对象状态的改变如何影响所有依赖它的对象?
    这里面以微信公众号为例,展开说明:

    • 假如用户A订阅了 某一个公众号G,那么当公众号G推送消息的时候,用户A就会收到相关的推送,点开可以查看推送的消息内容。
    • 但是公众号G并不关心订阅的它的是男人、女人还是二哈,它只负责发布自己的主体,只要是订阅公众号的用户均会收到该消息。
    • 作为用户A,不需要时刻打开手机查看公众号G是否有推动消息,因为在公众号推送消息的那一刻,用户A就会收到相关推送。
    • 当然了,用户A如果不想继续关注公众号G,那么可以取消关注,取关以后,公众号G再推送消息,A就无法收到了。
    发布-订阅模式抽象化

    上面即是对发布-订阅实例化的描述,但是跟上面问题的答案还是有些差距,我们付诸于代码,以代码的形式来模拟订阅消息、发布消息、取消订阅的功能,来解决上面提到的问题:

            // 01-定义一个订阅-发布模式函数;
            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-发布者;
                publish: function () {
                    let _self = this
                    // 获取发布行为
                    let type = Array.prototype.shift.call(arguments)
                    // 获取发布主题
                    let theme = Array.prototype.slice.call(arguments)
                    // 获取相关主题所有订阅者
                    let subscribes = _self._observer[type]
                    // 发布主题
                    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
                }
            }
            // 实例化发布-订阅模式
            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)
    
            // 实现发布、取订及再发布
            ps.publish('click', '第一次点击消息').unsubscrible('click', sub2).publish('click', '第二次点击消息')
            // 打印结果依次是:
            // sub1第一次点击消息
            // sub2第一次点击消息
            // sub1第二次点击消息
    

    上面代码块中,订阅者1 sub1 和 订阅者 sub2 分别订阅了 'click',这个行为,当发布者 ps.publish 发布主题的时候,sub1sub2 均收到了消息,在控制台输出 sub1第一次点击消息sub2第一次点击消息,然后 订阅者 sub2 又取订了 click 行为,所以当 发布者 ps.publish 再次发布主题的时候,只有 sub1 才收到相关消息。
    那么我们就通过代码阐述了依赖是如何建立的,就是通过订阅器来实现;

    但是,上述实现的代码存在两个问题:

    • 订阅行为需要在发布行为之前,如果直接发布主题,订阅器中没有相关的订阅行为,我这里手动抛出了警告。但是这是不应该的,正如用户A订阅了公众号G,也可以查看G的历史消息,所以这里需要实现查看发布主题历史记录的功能;
    • 其次,上述功能的实现是通过定义在一个自定义对象,这样就与发布-订阅模式的松散耦合理念有些出入,所以还需要做到如何更优雅的管理接口。
    发布-订阅模式优化版

    针对上述的问题,我在这个版本里面做了优化,看代码:

    // 声明一个全局发布-订阅对象,为不同模块之间的可能存在的通信做铺垫
    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
                }
                // 发布
                const publish = function () {
                    // 获取发布行为
                    let type = _shift.call(arguments)
                    // 获取发布主题
                    let theme = _slice.call(arguments)
                    // 记录发布主题
                    if (!_cache[type]) {
                        _cache[type] = [theme]
                    } else {
                        _cache[type].push(theme)
                    }
                    // 获取相关主题所有订阅者行为
                    let subscribes = _observer[type]
                    // 发布主题
                    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
                }
                // 查看发布记录
                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
                }
            }())
            // 先发布主题;
            Observer.publish('click', '第一次发布点击消息')
            Observer.publish('focus', '第一次发布聚焦消息')
            Observer.publish('blur', '第一次发布失焦消息')
    
            // 订阅
            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)
    
            // 再发布、取订、查看发布记录
            Observer.publish('click', '第二次发布点击消息').unsubscrible('click', sub2).publish('click', '第三次发布点击消息').publish('focus', '第二次发布聚焦消息').viewLog('click', function (message) {
                    console.log(message)
                })
    

    我们现在无论是先发布主题再订阅,还是订阅之后再发布主题,都不会有问题,因为在 Observer.publish 里面,发布者只关注自己发布主题功能,并且发布的时候将自己发布的对应主题保存。
    在发布功能里面添加一个存放发布记录的功能,在这里面我存放的是一个数组,是为了在 Observer.viewLog() 中方便调用。
    通过一系列的发布、取订、再发布、以及查看发布记录,打印结果如下:

    sub1第二次发布点击消息
    sub2第二次发布点击消息
    sub1第三次发布点击消息
    sub3第二次发布聚焦消息
    // 这是查看历史发布主题的结果,因为针对 click 行为,一共发布了三次主题
    第一次发布点击消息
    第二次发布点击消息
    第三次发布点击消息
    
    理解对象间一对多的依赖关系

    回到最初我们的问题,这个对象指的是既可以是自定义对象也可以是DOM对象

    • 定义两个模块
      let moduleA = {
              // 伪代码
              todo() {
                Observer.subscribe(type1, function (data) {
                    // 拿到 data 然后做一些事情
                })
            }
        }
      let moduleB = {
              // 伪代码
              todo() {
                Observer.subscribe(type1, function (data) {
                    // 拿到 data 然后做一些事情
                })
            }
        }
      // 下面是异步获取到数据
     // 伪代码
      ajax(function (data) {
            // 发布数据,所有的订阅均会拿到 data,然后按照自己的逻辑处理
            Observer.publish(type, data)
        })
    

    可能会有人疑问,为什么需要这样来传递数据,直接在 moduleAmoduleB 里面直接获取数据不可以吗?
    答案肯定是可以的,但是发布-订阅这种模式可以更优雅地在不同模块之间传递数据。

    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
      }
    }
    
    写在最后
    • 有人将观察者模式和发布-订阅模式认为是同一种模式,也有认为不是一种,仁者见仁,这里贴出一篇博客对两者的介绍: 观察者模式与发布/订阅模式区别
    • 关于本人实现的发布-订阅模式,仍存在问题,如果订阅行为过多,在团队协作中,会面临着命名冲突的局面,我就抛砖引玉,贴出大牛对这块逻辑的处理:JavaScript设计模式--观察者模式
    • 最后再贴出DMQ对vue响应式原理的实现过程:mvvm,如果想深入了解vue原理,是一个不错的过渡选择。
    • 关于发布-订阅模式,在 ES6 里面有了更好的实现,下次有时间的时候再继续分享。
    • 本文为原创文章,如果需要转载,请注明出处,方便溯源,如有错误地方,可以在下方留言,欢迎校勘,源码已上传到我的GitHub

    相关文章

      网友评论

        本文标题:发布者-订阅者模式简单实现

        本文链接:https://www.haomeiwen.com/subject/prmldftx.html