发布-订阅模式

作者: 会飞小超人 | 来源:发表于2018-12-17 20:57 被阅读5次

    发布—订阅模式又叫观察者模式,它定义对象间的一种一对多的依赖关系,当一个对象的状态发生改变时,所有依赖于它的对象都将得到通知。它有两个应用场景:

    • 可以广泛应用于异步编程中,替代回调函数。
    • 一个对象不用再显式的调用另一个对象的接口。让两个对象松耦合地联系在一起,虽然不太清楚彼此的细节,但这不影响它们之间相互通信。当有新的订阅者出现时,发布者的代码不需要任何修改;同样发布者需要改变时,也不会影响到之前的订阅者。只要之前约定的事件名没有变化,就可以自由地改变它们。
      DOM事件绑定就是典型的发布-订阅模式。

    第一版

    订阅者根据key来订阅自己感兴趣的事件。

    const salesOffices = {}
    
    salesOffices.clientList = []
    
    salesOffices.listen = function (key, fn) {
      if(!this.clientList[key]){
        this.clientList[key]=[]
      }
      this.clientList[key].push(fn)
    }
    
    salesOffices.trigger = function () {
      let key=Array.prototype.shift.call(arguments)
      let fns=this.clientList[key]
      if(!fns||fns.length===0){
        return
      }
      for (let i = 0, fn; fn = fns[i++];) {
        fn.apply(this, arguments)
      }
    }
    
    // 小明订阅消息
    salesOffices.listen('squareMeter88', function (price) {
      console.log('小明得到88平米的价格发布:' + price)
    })
    
    // 小红订阅消息
    salesOffices.listen('squareMeter100', function (price) {
      console.log('小红得到100平米的价格发布: ' + price)
    })
    
    // 小刚订阅消息
    salesOffices.listen('squareMeter88', function (price) {
      console.log('小刚得到88平米的价格发布:' + price)
    })
    
    salesOffices.trigger('squareMeter88',200000) // 小明得到88平米的价格发布:200000 小刚得到88平米的价格发布:200000
    salesOffices.trigger('squareMeter100',300000) //小红得到100平米的价格发布: 300000
    

    但是,上面代码存在几个问题:

    • 没有可扩展性,如果又有另外一个对象也需要这个模式,岂不是要复制一遍一模一样的代码?
    • 没有事件移除机制

    第二版

    相对第一版,增加下面功能:

    • 给对象动态增加发布订阅功能
    • 增加事件移除函数
    • 增加初始化绑定函数
    const event = {
      clientList: [],
      listen(key, fn) {
        if (!this.clientList[key]) {
          this.clientList[key] = []
        }
        this.clientList[key].push(fn)
    
      },
      trigger() {
        let key = Array.prototype.shift.call(arguments)
        let fns = this.clientList[key]
    
        if (!fns || fns.length === 0) { // 没有绑定对应的消息
          return false
        }
    
        for (let i = 0, fn; fn = fns[i++];) {
          fn.apply(this, arguments)
        }
      },
      // 清除已有事件队列,重新绑定
      one(key, fn) {
        this.remove(key)
        this.listen(key, fn)
      },
      remove(key, fn) {
        let fns = this.clientList[key]
    
        if (!fns) { // 如果key没有被人订阅,则直接返回
          return
        }
    
        if (!fn) { // 如果没有传回调函数,则表示取消key对应的所有的订阅
          fns.length = 0
        } else {
          for (let len = fns.length - 1; len >= 0; len--) {
            let _fn = fns[len]
            if (_fn === fn) {
              fns.splice(len, 1)
            }
          }
        }
      }
    }
    
    const installEvent = function (obj) {
      for (let i in event) {
        obj[i] = event[i]
      }
    }
    
    const salesOffices = {}
    installEvent(salesOffices)
    
    // 小明订阅消息
    salesOffices.listen('squareMeter88', ming = function (price) {
      console.log('小明得到88平米的价格发布:' + price)
    })
    
    // 小红订阅消息
    salesOffices.listen('squareMeter100', hong = function (price) {
      console.log('小红得到100平米的价格发布: ' + price)
    })
    
    // 小刚订阅消息
    salesOffices.listen('squareMeter88', gang = function (price) {
      console.log('小刚得到88平米的价格发布:' + price)
    })
    
    salesOffices.trigger('squareMeter88', 200000) // 小明得到88平米的价格发布:200000 小刚得到88平米的价格发布:200000
    salesOffices.trigger('squareMeter100', 300000) //小红得到100平米的价格发布: 300000
    
    salesOffices.remove('squareMeter88', gang)
    salesOffices.trigger('squareMeter88', 200000) // 小明得到88平米的价格发布:200000
    

    第二版已经比较的全面了,但是还是存在一些不足:

    • 给每一个发布者都要添加listen方法和trigger方法,以及clientList列表,浪费资源。
    • 订阅者需要知道发布者的名字,如果多个发布者发布同一个时间,那么订阅者需要订阅多次,这显然不是订阅者希望看到的,因为他只关心事件本身,而非事件的发布者。

    第三版

    可以把事件发布完全委托给第三方管理,而非事件的本身发布者。所以可以这样写:

    const Event=(function(){
      let clientList=[],
      listen,
      trigger,
      remove
    
      listen=function(key,fn){
        if(!clientList[key]){
          clientList[key]=[]
        }
        clientList[key].push(fn)
      }
    
      trigger=function(){
        let key=Array.prototype.shift.call(arguments)
        let fns=clientList[key]
        if(!fns||fns.length===0){
          return 
        }
        for(let i=0,fn;fn=fns[i++];){
          fn.apply(this,arguments)
        }
      }
    
      remove=function(key,fn){
        let fns=clientList[key]
        if(!fns){
          return
        }
        if(!fn){
          fns.length=0
        }else{
          for(let len=fns.length-1;len>=0;len--){
            let _fn=fns[len]
            if(_fn===fn){
              fns.splice(len,1)
            }
          }
        }
      }
    
      return{
        listen,
        trigger,
        remove
      }
    
    })()
    
    // 小明订阅消息
    Event.listen('squareMeter88',ming=function (price){
      console.log('小明得到88平米的价格发布:'+price)
    })
    
    // 小刚订阅消息
    salesOffices.listen('squareMeter88', gang = function (price) {
      console.log('小刚得到88平米的价格发布:' + price)
    })
    
    
    Event.trigger('squareMeter88',2000000) // 小明得到88平米的价格发布:200000 小刚得到88平米的价
    Event.remove('squareMeter88',ming)
    Event.trigger('squareMeter88',2000000) // 小明得到88平米的价格发布:200000 
    

    这样就有了一个全局的事件管理对象,发布者和订阅者不用直接通信,而是通过这个第三方对象来进行事件的发布订阅。同时也真正实现了聚焦事件本身。

    模式四-高级功能

    • 全局的发布—订阅对象里只有一个clinetList来存放消息名和回调函数,大家都通过它来订阅和发布各种消息,久而久之,难免会出现事件名冲突的情况,所以我们还可以给Event对象提供创建命名空间的功能。
    • 另外,还有一点就是,我们想实现离线消息的功能。就是发布者可以现发布消息,然后等到订阅者订阅后,先完成发布者的离线消息确认,然后再进行常规的发布-订阅操作。
    const Event = (function () {
      let _default = 'default'
    
      let Event = function () {
        // 这里的私有方法表示单纯的事件方法,不包含命名空间和离线消息功能
        let _listen,
          _trigger,
          _remove,
          _shift = Array.prototype.shift,
          _unshift = Array.prototype.unshift,
          namespaceCache = {},
          _create,
          each = function (arr, fn) {
            let ret
            for (let i = 0, len = arr.length; i < len; i++) {
              let n = arr[i]
              ret = fn.call(n, i, n)
            }
            return ret
          }
    
        _listen = function (key, fn, cache) {
          if (!cache[key]) {
            cache[key] = []
          }
          cache[key].push(fn)
        }
    
        _remove = function (key, cache, fn) {
          if (cache[key]) {
            if (fn) {
              for (let len = cache[key].length - 1; len >= 0; len--) {
                if (cache[key][len] === fn) {
                  cache[key].splice(len, 1)
                }
              }
            } else {
              cache[key].length = 0
            }
          }
        }
    
        _trigger = function () {
          let cache = _shift.call(arguments)
          let key = _shift.call(arguments)
          let args = arguments
          let stack = cache[key]
          let _self = this
    
          if (!stack || !stack.length) {
            return
          }
    
          return each(stack, function () {
            return this.apply(_self, args)
          })
        }
    
        _create = function (namespace = _default) {
          let cache = {}
          let offlineStack = []
          let ret = {
            // 这里的方法是对上面的原始方法进行封装,混入离线消息和命名空间逻辑
            listen(key, fn, last) {
              _listen(key, fn, cache)
              if (offlineStack === null) {
                return
              }
              if (last === 'last') { // 表示弹出并执行离线队列的最后一个
                offlineStack.length && offlineStack.pop()()
              } else {
                each(offlineStack, function () {
                  this()
                })
              }
              offlineStack = null
            },
            // 清除事件队列的所有函数,然后调用listen函数
            // remove all + listen
            one(key, fn, last) {
              _remove(key, cache)
              this.listen(key, fn, last)
            },
            remove(key, fn) {
              _remove(key, cache, fn)
            },
            trigger() {
              let fn
              let args
              let _self = this
              _unshift.call(arguments, cache)
              args = arguments
              fn = function () {
                return _trigger.apply(_self, args)
              }
    
              if (offlineStack) {
                return offlineStack.push(fn)
              }
              return fn()
            }
          }
          // 外面一层,检验namespace参数是否能作为对象属性,如果不能,则不创建命名空间,直接返回一个新的对象,但这种场景没有任何作用,所以可以简单理解为校验namespace参数
          // 里面一层,检验这个命名空间是否存在,如果存在则返回已存在的命名空间,否则创建一个新的命名空间,并返回这个命名空间
          return namespace ? (namespaceCache[namespace] ? namespaceCache[namespace] : namespaceCache[namespace] = ret) : ret
        }
        return {
          create: _create, // 创建命名空间对象以及这个对象的各种方法
          one: function (key, fn, last) {
            var event = this.create();
            event.one(key, fn, last);
          },
          remove: function (key, fn) {
            var event = this.create();
            event.remove(key, fn);
          },
          listen: function (key, fn, last) {
            var event = this.create();
            event.listen(key, fn, last);
          },
          trigger: function () {
            var event = this.create();
            event.trigger.apply(this, arguments);
          }
        }
      }
      return Event()
    })()
    

    这里可以有几种用法:

    1. 使用者可以自己创建命名空间,然后在自己的命名空间里管理消息。
    Event.create( 'namespace1' ).listen( 'click', function( a ){
        console.log( a );    // 输出:1
    });
    
    Event.create( 'namespace1' ).trigger( 'click', 1 );
    
    1. 使用者也可以不用自己创建空间,直接使用默认的命名空间。
    Event.listen( 'click', function( a ){
        console.log( a );       // 输出:1
    });
    Event.trigger( 'click', 1 );
    
    1. 使用者可以先发布离线消息,然后在订阅者订阅的时候自动处理。
    Event.trigger( 'click', 1 );
    
    Event.listen( 'click', function( a ){
        console.log( a );       
    }); // 1
    
    Event.trigger( 'click', 2 ); // 2
    

    发布—订阅模式的优点非常明显,一为时间上的解耦,二为对象之间的解耦。它的应用非常广泛,既可以用在异步编程中,也可以帮助我们完成更松耦合的代码编写。发布—订阅模式还可以用来帮助实现一些别的设计模式,比如中介者模式。 从架构上来看,无论是MVC还是MVVM,都少不了发布—订阅模式的参与,而且JavaScript本身也是一门基于事件驱动的语言。

    相关文章

      网友评论

        本文标题:发布-订阅模式

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