js经典设计模式--发布订阅模式

1,080 阅读6分钟

什么是发布-订阅模式

  • 发布—订阅模式又叫观察者模式,它定义对象间的一种一对多的依赖关系,当一个对象的状态发生改变时,所有依赖于它的对象都将得到通知。
  • 举个例子,售楼处卖房,那么售楼处要发布房型信息,那么它是发布者,中介关注房型,所以中介是订阅者,当售楼处发布消息之后或者房型信息更新之后,中介就会收到消息。紧接着他去通知客户。这么做的好处是:客户不用关心房型,不用和任何一家售楼处保持紧密的联系,只需要与某个中介联系,但是他可以通过中介知道所有房型的变化。所有客户与售楼处是没有耦合关系的。
  • 如下图:

在这里插入图片描述
同样的,我们可以把它运用到编程上面来,降低代码的耦合性。
我们试着使用发布订阅模式实现以上功能。

  let houseObj = {}; //定义发布者
  houseObj.list = []; //缓存列表 (花名册) 存放订阅者回调函数
  //增加订阅者
  houseObj.listen = function(fn){
  //订阅消息添加到缓存列表
     this.list.push(fn); 
   }
  //发布消息 是不是要遍历这个列表
   houseObj.trigger = function(){
    for(let i = 0,fn; fn = this.list[i++];){
            fn.apply(this,arguments); // arguments 是发布消息时附送的参数
        }
   }
  //小红的要求 (订阅)
  houseObj.listen(function(size){
    console.log('小红:我要的房子是'+size+'平米');
  })

  //小绿的要求 (订阅)
   houseObj.listen(function(size){
     console.log('小绿:我要的房子是'+size+'平米');
   })
  //执行
  houseObj.trigger(100);
  houseObj.trigger(150);

在这里插入图片描述

存在的问题

  • 写完这段代码之后,运行一下,会发现它打印了四次,而我们理想结果是2次,因为houseObj把所有的消息都发给某一个订阅者了。

  • 这样对于用户来说是十分不友好的,他只想看他订阅的那条消息。那我们优化一下。

    let houseObj = {}; //发布者
    houseObj.list = {}; //缓存列表 (花名册) 存放订阅者回调函数
    
    //增加订阅者
    houseObj.listen = function(key,fn){   //增加一个唯一标识key
    	//如果没有订阅过此消息 给该消息创建一个缓存列表
      	(this.list[key] || (this.list[key] = [])).push(fn)
    }
    
    //发布消息 遍历列表
    houseObj.trigger = function(){
      //取出消息类型名称
      let key = Array.prototype.shift.call(arguments);
      
      // 取出该消息对应的回调函数的集合
      let fns = this.list[key];
      if(!fns || fns.length === 0){
        return;
      }
      for(let i = 0,fn; fn = fns[i++];){
              fn.apply(this,arguments); // arguments 是发布消息时附送的参数
          }
    }
    //小红的要求 (订阅)
    houseObj.listen('big',function(size){
      console.log('小红:我要的房子是'+size+'平米');
    })
    
     //小绿的要求 (订阅)
     houseObj.listen('small',function(size){
      console.log('小绿:我要的房子是'+size+'平米');
    })
    //执行
    houseObj.trigger('big',100);
    houseObj.trigger('small',150);
    
  • 这样我们就会看到控制台只打印了两次,传了一个唯一的key。

  • 我们知道,对于上面的代码,对于不同的用户去买房子这么一个对象houseObj 进行订阅,但是如果以后我们需要对买水果子买鞋子或者其他的对象进行订阅呢,我们如果每次都这么去写是不是会很麻烦?

  • 我们需要复制上面的代码,再重新改下里面的对象代码;这样是很麻烦的。为此我们需要进行代码封装 :

// 定义一个对象
 let event = {
    list:{},
    listen: function(key,fn){   //增加一个唯一标识key
      //如果没有订阅过此消息 给该消息创建一个缓存列表
        (this.list[key] || (this.list[key] = [])).push(fn)
    },
   trigger: function(){
      //取出消息类型名称
      let key = Array.prototype.shift.call(arguments);
      let fns = this.list[key];
      if(!fns || fns.length === 0){
        return;
      }
      for(let i = 0,fn; fn = fns[i++];){
        fn.apply(this,arguments); // arguments 是发布消息时附送的参数
      }
    }
  }
  //定义一个initEvent函数,这个函数使所有的普通对象都具有发布订阅功能
  let initEvent = function(obj){
      for(let i in event){
        obj[i] = event[i];
      }
  };
  let houseObj = {};   //发布者对象
  initEvent(houseObj); //为对象添加发布-订阅功能     

  //小明订阅的消息
  houseObj.listen('big',function(size){
    console.log('小明:我要的房子是'+size+'平米');
  })
   //小绿订阅的消息
   houseObj.listen('small',function(size){
    console.log('小绿:我要的房子是'+size+'平米');
  })
  houseObj.trigger('big',100);
  houseObj.trigger('small',150);

如上,我们只需要调用initEvent,便可以使所有对象都拥有发布订阅模式。
那么,接下来,如果某用户不想订阅了
在这里插入图片描述

如何取消订阅?

//删除订阅
  event.remove = function(key,fn){
     let fns = this.list[key];
     //如果没有定阅过 直接返回false
     if(!fns){
       return false;
     }
     // 如果没有传入具体的回调函数,表示需要取消key对应消息的所有订阅
     if(!fn){
       fn && (fns.length = 0);
     }else{
       for(let i = fns.length - 1;i >= 0; i-- ){
         let _fn = fns[i];
         _fn === fn && (fns.splice(i,1));  //删除订阅者对应的回调函数
       }
     }
   }
   //其余保持不变
   //小明订阅的第一条消息
   houseObj.listen('big',fn1 = function(size){
    console.log('小明:我要的第一套房子是'+size+'平米');
  })
   //小明订阅的第二条消息
   houseObj.listen('big',fn2 = function(size){
    console.log('小明:我要的第二套房子是'+size+'平米');
  })
  //删除第二条
  houseObj.remove('big',fn2);
  houseObj.trigger('big',100);

这样,控制台就会只有一条打印了,只有第一条消息还在。

继续深度解耦

1.我们给每个发布者对象都添加了 listen 和 trigger 方法,以及一个缓存列表 list,这其实是一种资源浪费。
2.小明跟售楼处对象还是存在一定的耦合性,小明至少要知道售楼处对象的名字是houseObj ,要知道房型是big,还是small或是normal才能顺利的订阅到事件。
所以我们继续优化,封装一个全局发布-订阅模式对象

封装全局发布-订阅模式对象

  let Event = (function(){
    let list = {},
        listen,
        trigger,
        remove;
        listen = function(key,fn){
          (list[key] || (list[key] = [])).push(fn);
        };
        trigger = function(){
          let key = Array.prototype.shift.call(arguments),
          // 取出该消息对应的回调函数的集合
              fns = list[key];
          if(!fns || fns.length === 0){
            return false;
          }
          for(let i = 0,fn; fn = fns[i++];){
            fn.apply(this,arguments); // arguments 是发布消息时附送的参数
          }
        };
        remove = function(key,fn){
          let fns = list[key];
          //如果没有定阅过 直接返回false
          if(!fns){
            return false;
          }
          // 如果没有传入具体的回调函数,表示需要取消key对应消息的所有订阅
          // 小明在售楼处不买了 取消了 说要买三室一厅,四室一厅,结果都是吹牛皮
          if(!fn){
            fn && (fns.length = 0);
          }else{
            for(let i = fns.length - 1;i >= 0; i-- ){
              let _fn = fns[i];
              _fn === fn && (fns.splice(i,1));  //删除订阅者对应的回调函数
            }
          }
        };
        return {
          listen:listen,
          trigger:trigger,
          remove:remove
        }
  })();

  Event.listen('big',function(size){
    console.log('小明想要的房型大小是'+size+'平米');
  })
  Event.trigger('big',100);

这样,用户连售楼处是哪都不用管了,高度解耦
在这里插入图片描述

FAQ

1.创建订阅者本身要消耗一定的时间和内存,而且当你订阅一个消息后,也许此消息最后都未发生,但这个订阅者会始终存在于内存中。另外,发布—订阅模式虽然可以弱化对象之间的联系,但如果过度使用的话,对象和对象之间的必要联系也将被深埋在背后,会导致程序难以跟踪维护和理解。特别是有多个发布者和订阅者嵌套到一起的时候,要跟踪一个 bug 不是件轻松的事情。
2.不要滥用