发布-订阅模式

147 阅读5分钟

背景

react 的工作模式是单向数据流:指的就是当前组件的 state 以 props 的形式流动时,只能流向组件树中比自己层级更低的组件。

  • 父子通信:可以通过父组件传递 props 给子组件来实现
  • 子父通信:考虑到 props 是单向的,子组件并不能直接将自己的数据塞给父组件,但 props 的形式也可以是多样的。假如父组件传递给子组件的是一个绑定了自身上下文的函数,那么子组件在调用该函数时,就可以将想要交给父组件的数据以函数入参的形式给出去,以此来间接地实现数据从子组件到父组件的流动。
  • 兄弟间通信:需要有一个先决条件,兄弟组件都拥有一个共同的父组件,然后使用父子通信和子父通信的方法实现兄弟间通信

三种通信可以解决大部分的场景,但是假如组件间嵌套的十分深,想通过层层传递 props 的方式解决通信就不合适了,往往会消耗非常多的代码,而且很难维护。

浏览器原生发布-订阅模式

发布-订阅机制早期最广泛的应用,应该是在浏览器的 DOM 事件中。

target.addEventListener(type, listener, useCapture);

通过调用 addEventListener 方法,我们可以创建一个事件监听器,这个动作就是“订阅”。比如我可以监听 click(点击)事件:

el.addEventListener("click", func, false);

这样一来,当 click 事件被触发时,事件会被“发布”出去,进而触发监听这个事件的 func 函数。这就是一个最简单的发布-订阅案例。

使用发布-订阅模式的优点在于,监听事件的位置和触发事件的位置是不受限的,就算相隔十万八千里,只要它们在同一个上下文里,就能够彼此感知。这个特性,太适合用来应对“任意组件通信”这种场景了。

实现发布-订阅模式

发布-订阅模式中有两个关键的动作:事件的监听(订阅)和事件的触发(发布) ,这两个动作自然而然地对应着两个个基本的 API 方法;还有一个事件删除的 API 方法。

  • on():负责注册事件的监听器,指定事件触发时的回调函数。
  • emit():负责触发事件,可以通过传参使其在触发的时候携带数据 。
  • off():负责监听器的删除。

整个发布-订阅模式都被封装在一个类,叫做 EventEmitter,事件和该事件的对应关系需要通过一个容器存储,设为对象 eventMap。

constructor() {
  // eventMap 用来存储事件和监听函数之间的关系
  this.eventMap= {}
}

订阅 on()

所谓“订阅”,也就是注册事件监听函数的过程。这是一个“写”操作,具体来说就是把事件和对应的监听函数写入到 eventMap 里面去:

// type 这里就代表事件的名称
on(type, handler) {

  // hanlder 必须是一个函数,如果不是直接报错
  if(!(handler instanceof Function)) {
    throw new Error("大哥,请你传一个函数")
  }

  // 判断 type 事件对应的队列是否存在
  if(!this.eventMap[type]) {
   // 若不存在,新建该队列
    this.eventMap[type] = []
  }

  // 若存在,直接往队列里推入 handler
  this.eventMap[type].push(handler)
}

发布 emit()

订阅操作是一个“写”操作,相应的,发布操作就是一个“读”操作。发布的本质是触发安装在某个事件上的监听函数,我们需要做的就是找到这个事件对应的监听函数队列,将队列中的 handler 依次执行出队:

// 触发时是可以携带数据的,params 就是数据的载体
emit(type, param) {

  // 假如该事件没有订阅,抛出异常
  if(!this.eventMap[type]) {
  throw new Error("大哥,不存在这个事件")
  }
  
  // 假设该事件是有订阅的(对应的事件队列存在)
  if(this.eventMap[type]) {
    // 将事件队列里的 handler 依次执行出队
    this.eventMap[type].forEach((handler, index)=> {
      // 读取 params
      handler(params)
    })
  }

解除 off()

off(type, handler) {
  if(this.eventMap[type]) {
    this.eventMap[type].splice(this.eventMap[type].indexOf(handler)>>>0,1)
  }
}

splice 的第一个参数是负数时,会从数组的最后往前找。如果传入一个不存在的函数给 off 方法,indexOf 找不到会返回 -1 ,再调用 splice 就会将队列中最后一个函数删除掉了。而使用无符号右移,-1 无符号右移的结果为 4294967295,这个数足够大,不会对原队列造成影响。

类 EventEmitter

class myEventEmitter {
  constructor() {
    // eventMap 用来存储事件和监听函数之间的关系
    this.eventMap = {};
  }

  // type 这里就代表事件的名称
  on(type, handler) {

    // hanlder 必须是一个函数,如果不是直接报错
    if (!(handler instanceof Function)) {
      throw new Error("哥 你错了 请传一个函数");
    }

    // 判断 type 事件对应的队列是否存在
    if (!this.eventMap[type]) {
      // 若不存在,新建该队列
      this.eventMap[type] = [];
    }
    
    // 若存在,直接往队列里推入 handler
    this.eventMap[type].push(handler);
  }

  // 触发时是可以携带数据的,params 就是数据的载体
  emit(type, params) {
  
    // 假如该事件没有订阅,抛出异常
    if(!this.eventMap[type]) {
      throw new Error("大哥,不存在这个事件")
    }
  
    // 假设该事件是有订阅的(对应的事件队列存在)
    if (this.eventMap[type]) {
      // 将事件队列里的 handler 依次执行出队
      this.eventMap[type].forEach((handler, index) => {
        // 读取 params
        handler(params);
      });
    }
  }

  off(type, handler) {
    if (this.eventMap[type]) {
    this.eventMap[type].splice(this.eventMap[type].indexOf(handler) >>> 0, 1);
    }
  }
}