JavaScript设计模式之----原生JS实现简单的发布订阅模式

 第一部分: 发布订阅模式简介

发布—订阅模式又叫观察者模式,它定义对象间的一对多的依赖关系,当一个对象的状态发生改变时,所有依赖于它的对象都将得到通知。在javascript开发中,一般用事件模型来替代传统的发布—订阅模式。

发布—订阅模式可以广泛应用于异步编程中,是一种替代传递回调函数的方案。比如,可以订阅ajax请求的error、success等事件。或者如果想在动画的每一帧完成之后做一些事情,可以订阅一个事件,然后在动画的每一帧完成之后发布这个事件。在异步编程中使用发布—订阅模式,就无需过多关注对象在异步运行期间的内部状态,而只需要订阅感兴趣的事件发生点

第二部分:发布订阅模式在DOM编程操作过程中的使用

  发布—订阅模式可以取代对象之间硬编码的通知机制,一个对象不用再显式地调用另外一个对象的某个接口。发布—订阅模式让两个对象松耦合地联系在一起,虽然不太清楚彼此的细节,但这不影响它们之间相互通信。当有新的订阅者出现时,发布者的代码不需要任何修改;同样发布者需要改变时,也不会影响到之前的订阅者。只要之前约定的事件名没有变化,就可以自由地改变它们

实际上,前端开发中常用的,在DOM节点上面绑定事件函数,就属于发布—订阅模式

document.body.addEventListener('click',function(){
  alert(2);
},false);
document.body.click();

如果需要监控用户点击document.body的动作,但是没办法预知用户将在什么时候点击。所以订阅document.body上的click事件,当body节点被点击时,body节点便会向订阅者发布这个消息。

同理,其实DOM中的很多事件操作都是采用的这个原理

document.body.addEventListener('keyup',function(){
  alert(2);
},false);
document.body.keyup();

document.body.addEventListener('mousedown',function(){
  alert(2);
},false);
document.body.mousedown();

还可以为单个事件添加多个监听功能,

document.body.addEventListener('click',function(){
  console.log(2);
},false);
document.body.addEventListener('click',function(){
 console.log(3);
},false);
document.body.addEventListener('click',function(){
  console.log(4);
},false);
document.body.click();    //模拟用户点击

// 2, 3, 4

第三部分:发布订阅模式的其他使用场景

假设有一个店铺,出售Iphone,会有多种型号的Iphone出售,而消费者也会有不同的需求,如果每个消费者都要来向店员询问自己需要的款型的价格,那么这是一个很低效的行为,因为消费者最关心的就是型号和价格,这样用发布订阅模式就最合适不过了

const  shop = {}; // 首先定义一个商铺

shop.list = [];  // 定义商铺里的商品信息列表

shop.listen = function(fn) { // 添加订阅者
    this.list.push(fn); // 将订阅的商品添加进入商品心里列表
}  

shop.sell = function(){
    for( var i = 0, fn; fn = this.list[ i++ ]; ){
        fn.apply( this, arguments ); // (2) // arguments 是发布消息参数
    }
}

// 这是来了一个顾客询问手机的价格,那么
shop.listen(function(iphone, price) {
    console.log('手机型号' + iphone);
    console.log('价格' + price)
})

// 发布消息,本店卖IphoneX, 价格7000
shop.sell('IphoneX', 7000);

shop.sell('Iphone11', 9000);

// 输出 手机型号IphoneX, 价格7000
// 输出 手机型号Iphone11, 价格9000

现在我们已经实现了一个最简单的发布订阅模式了,但这里还存在一些问题。订阅者接收到了发布者发布的每个消息,如果我只想买Iphone11,我是不关心IphoneX的价格的,但是发布者把IphoneX的信息也推送给了我,这对我来说是不必要的困扰。所以有必要增加一个标示key,让订阅者只订阅自己感兴趣的消息。改写后的代码如下:

const  shop = {}; // 首先定义一个商铺

shop.list = {};  // 定义商铺里的商品信息列表

shop.listen = function(key, fn) { // 添加订阅者
    if ( !this.list[key] ){ // 如果没有订阅,创建一个缓存列表
        this.list[key] = [];
    }
    this.list[key].push( fn ); // 订阅的消息添加进消息缓存列表
}  

shop.sell = function(){
   const key = Array.prototype.shift.call( arguments );// 取出消息
   const fns = this.list[ key ]; // 取出该消息对应的回调函数集合
    if ( !fns || fns.length === 0 ){ // 如果没有订阅该消息,则返回
        return false;
    }
    for( var i = 0, fn; fn = fns[ i++ ]; ){
        fn.apply( this, arguments ); // (2) // arguments 是参数
    }
}

// 这是来了一个顾客询问手机的价格,那么
shop.listen('IphoneX', function(price) {
    console.log('价格' + price)
})

// 这是来了一个顾客询问手机的价格,那么
shop.listen('Iphone11', function(price) {
    console.log('价格' + price)
})


// 发布消息,本店卖IphoneX, 价格7000
shop.sell('IphoneX', 7000);

shop.sell('Iphone11', 9000);

// 输出 价格7000
// 输出 价格9000

依照上面的例子,我们就可以写一个基于对象的发布订阅的模型了

const event = {
    clientList: [],
    listen: function( key, fn ){
        if ( !this.clientList[ key ] ){
            this.clientList[ key ] = [];
        }
            this.clientList[ key ].push( fn ); // 订阅的消息添加进缓存列表
        },
        trigger: function(){
            var key = Array.prototype.shift.call( arguments ), // (1);
            fns = this.clientList[ key ];
            if ( !fns || fns.length === 0 ){ // 如果没有绑定对应的消息
                return false;
            }
            for( var i = 0, fn; fn = fns[ i++ ]; ){
                fn.apply( this, arguments ); // (2) // arguments
            }
        },
        remove: function(key, fn) {
              var fns = this.clientList[ key ];
              if ( !fns ){ // 如果key 被人订阅,则直接返回
                 return false;
              }
              if ( !fn ){ // 如果没有传入具体函数,表示需要取消所有订阅
                 fns && ( fns.length = 0 );
              }else{
                  for ( var l = fns.length - 1; l >=0; l-- ){ 
                      var _fn = fns[ l ];
                      if ( _fn === fn ){
                         fns.splice( l, 1 ); // 删除订阅者的回调函数
                      }
                  }
             }
        }
    };

// 下面是一个基于class的发布订阅模式的模版,考虑到了边界条件和匿名函数,属于一个比较完整的实现

class Pubsub {
  constructor () {
  }

  list = {};

  // 添加消息监听的方法
  subscribe (topic, func) {
    if (typeof topic !== 'string') {
      throw 'topic为字符串类型'
    }
    if (typeof func !== 'function') {
      throw 'func为函数类型'
    }
    const list = this.list;
    if (!list[topic]) {
      list[topic] = [];
    }
    list[topic].push(func);
// 为了防止匿名函数的影响,在添加时将取消监听的方法返回 return () => this.unsubscribe(topic, func); } // 发布消息的方法 publish (topic, data) { if (typeof topic !== 'string') { throw 'topic必须是字符串类型' } const list = this.list; if(!list[topic]) { throw '不存在该事件的监听' } else { list[topic].forEach((func)=>{ func.call(this, data) }) } } // 移除消息监听的方法 unsubscribe (topic, func){ if(typeof topic !== 'string') { throw 'topic为字符串类型' } if(func && (typeof func !== 'function')) { throw 'func为函数类型' } const list = this.list; if(!list[topic]) { throw '不存在该topic监听' } if(!func) { // 如果没有第二个参数,就移除所有的监听事件 if(list[topic]) { delete list[topic] } } else { if(!list[topic].includes(func)) { throw '要移除的事件不存在' } else { const index = list[topic].findIndex(item => item === func); list[topic].splice(index, 1); if(list[topic].length === 0) { delete list[topic] } } } } }

猜你喜欢

转载自www.cnblogs.com/liquanjiang/p/11724793.html
今日推荐