你不知道的JavaScript

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

2020-02-21  本文已影响0人  oneSailboat

发布订阅模式是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
基于上面的理解,我们可以汇总得出,事件池需要有以下方法:

接下来我们自己写一个发布订阅模式的事件池类,摆脱对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;
        }
      }
    }
    //+++++++++++++++++
    
    
  }
}();

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

  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();
  }
  //+++++++++++++++++

  
}();

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

let pond = _subscribe();

这句话做了个啥?

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

More Idea?请关注个人博客 syfless

参考资料

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

猜你喜欢

热点阅读