美文网首页你不知道的JavaScript
JavaScript设计模式·自己实现一个发布订阅的事件池

JavaScript设计模式·自己实现一个发布订阅的事件池

作者: oneSailboat | 来源:发表于2020-02-21 18:32 被阅读0次

    发布订阅模式是JavaScript常见的模式之一,他的根本思想在于:

    通过一个事件池给元素的触发事件绑定多个方法(方法不可重复),后续扩展时只需往事件池中添加方法即可

    下面给出一个jquery的事件池使用案例,方便理解
    (ps: 每段代码“++++”中间)

    // 初始化一个事件池
    let pond = $.Callbacks();
    
    // 在某个触发方法中启动事件池,再方法触发时会按顺序执行事件池中的方法
    $("#button").click(function(){
      // fire函数可带有参数,参数会传递给事件池中的所有函数
      pond.fire();
    });
    
    let f1 = function(){console.log(1)};
    let f2 = function(){console.log(2)};
    let f3 = function(){console.log(3)};
    
    // 向事件池中添加方法
    pond.add(f1);
    pond.add(f2);
    pond.add(f3);
    
    // 删除方法
    pond.remove(f2);
    

    上述方法执行之后会依次输出1 3

    image
    基于上面的理解,我们可以汇总得出,事件池需要有以下方法:
    • 通过一个函数(Callbacks)进行初始化
    • 通过一个函数添加到某个方法的执行体中(fire
    • 可以按顺序添加(add)和删除(remove

    接下来我们自己写一个发布订阅模式的事件池类,摆脱对jquery的依赖

    1. 首先形成闭包,放置变量污染
    let _subscribe = function(){
      
      
    }();
    
    1. 创建一个发布订阅类Sub类,并实现构造函数
    let _subscribe = function(){
      
      //+++++++++++++++++
      class Sub{
        // 构造函数:初始化一个函数数组
        constructor(){
          this.pond = [];
        }
        
      }
      //+++++++++++++++++
    
    
    }();
    
    1. 实现添加方法和删除方法
    let _subscribe = function(){
      class Sub{
        // 构造函数:初始化一个函数数组
        constructor(){
          this.pond = [];
        }
        
        //+++++++++++++++++
        // 添加函数
        add(func){
          // 判断是否是“函数”
          if(typeof func !== "function"){
            return;
          }
          // 判断是否与已经添加的方法重复
          let flag = this.pond.some(item => item===func);
          !flag?this.pond.push(func):null;
        }
        
        // 删除函数
        remove(func){
          for(let i=0;i<pond.length;i++){
            let item = pond[i];
            if(item===func){
              pond[i] = null;
              break;
            }
          }
        }
        //+++++++++++++++++
        
        
      }
    }();
    

    这里有一些需要注意的地方:

    • add中需要做两层检测:类型检测、重复检测
    • remove为什么不用splice? splice会导致一种很隐秘的数组塌陷:当添加的方法中自带对其他方法的删除时会导致数组塌陷,结合下方的fire便知
    1. 实现点燃函数,使函数依次执行
    let _subscribe = function(){
      class Sub{
        // 构造函数:初始化一个函数数组
        constructor(){
          this.pond = [];
        }
        
        // 添加函数
        add(func){
          // 判断是否是“函数”
          if(typeof func !== "function"){
            return;
          }
          // 判断是否与已经添加的方法重复
          let flag = this.pond.some(item => item===func);
          !flag?this.pond.push(func):null;
        }
        
        // 删除函数
        remove(func){
          for(let i=0;i<pond.length;i++){
            let item = pond[i];
            if(item===func){
              pond[i] = null;
              break;
            }
          }
        }
        
        //+++++++++++++++++
        // 点燃函数 args用于接收参数
        fire(...args){
          let pond = this.pond;
          for (let i = 0; i < pond.length; i++){
            let item = pond[i];
            if (typeof item !== "function") {
              // 此时再删除
              pond.splice(i, 1);
              i--;
              continue;
            }
            // 在三个参数以上的情况下,call的性能略优于apply
            item.call(this, ...args);
          }
        }
        //+++++++++++++++++
    
      }
    }();
    

    fire函数中将之前被remove的方法删除,这样就不会造成数组塌陷问题

    在绑定this和参数时有一个小细节,【用call而不用apply

    这是因为call在参数有3个以上时性能优于apply,为什么呢?

    因为apply其实也要在最后调用call,但是在调用之前要做一系列对数组格式的校验,这些校验的时间会造成性能的浪费

    1. 将调用方法暴露在闭包外

    ​ 这里我们不需要用户通过构造函数重建一个对象,而是在闭包构造对象,然后让外界使用内部对象的方法,具体写起来只需要return一个函数方法即可

    let _subscribe = function(){
      class Sub{
        // 构造函数:初始化一个函数数组
        constructor(){
          this.pond = [];
        }
        
        // 添加函数
        add(func){
          // 判断是否是“函数”
          if(typeof func !== "function"){
            return;
          }
          // 判断是否与已经添加的方法重复
          let flag = this.pond.some(item => item===func);
          !flag?this.pond.push(func):null;
        }
        
        // 删除函数
        remove(func){
          for(let i=0;i<pond.length;i++){
            let item = pond[i];
            if(item===func){
              pond[i] = null;
              break;
            }
          }
        }
        
        // 点燃函数 args用于接收参数
        fire(...args){
          let pond = this.pond;
          for (let i = 0; i < pond.length; i++){
            let item = pond[i];
            if (typeof item !== "function") {
              // 此时再删除
              pond.splice(i, 1);
              i--;
              continue;
            }
            // 在三个参数以上的情况下,call的性能略优于apply
            item.call(this, ...args);
          }
        }
      }
      
      //+++++++++++++++++
      // 暴露一个方法
      return function subscribe(){
        return new Sub();
      }
      //+++++++++++++++++
    
      
    }();
    

    这里可能被闭包来闭包去给绕晕,解释一下

    • 这段代码最上面的_subscribe最终会赋值这个大闭包return的对象,也就是返回一个在闭包作用域中生成的对象地址
    • 这个对象地址是一个方法,也就是最后的subscribe函数
    • 我们在使用的时候会执行下列语句
    let pond = _subscribe();
    

    这句话做了个啥?

    _subscribe执行的结果返回给pond,也就是说其实pond被赋了一个闭包中的闭包里存放的对象地址

    More Idea?请关注个人博客 syfless

    参考资料

    1. 珠峰前端相关教程
    2. 【 js 基础 】为什么 call 比 apply 快?

    相关文章

      网友评论

        本文标题:JavaScript设计模式·自己实现一个发布订阅的事件池

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