zoukankan      html  css  js  c++  java
  • 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] } } } } }
  • 相关阅读:
    常见数据结构使用场景
    Java与算法之(4)
    Java与算法之(3)
    Java与算法之(2)
    Java与算法之(1)
    Maven适配多种运行环境的打包方案
    从头开始基于Maven搭建SpringMVC+Mybatis项目(4)
    从头开始基于Maven搭建SpringMVC+Mybatis项目(3)
    从头开始基于Maven搭建SpringMVC+Mybatis项目(2)
    从头开始基于Maven搭建SpringMVC+Mybatis项目(1)
  • 原文地址:https://www.cnblogs.com/liquanjiang/p/11724793.html
Copyright © 2011-2022 走看看