一文玩转 mini-signals 发布-订阅模式库

974 阅读5分钟

一、前言

  mini-signals 是一款轻量、快速的发布-订阅模式库。对于一名合格的前端仔,应该对发布-订阅模式了然于胸。

发布-订阅模式定义了对象间一对多的依赖关系,可以用来解决对象间耦合的问题。

  从 DOM 的事件系统,到 Node.js 的 events 模块,再到目前流行的 MVVM 架构模式,发布-订阅模式无处不在。

  利用 JavaScript 的一些基础知识,可以快速地实现一个简易的发布-订阅模式:

  function EventEmitter () {
    this.events = {};
  }

  EventEmitter.prototype.on = function (event, fn) {
    const { events } = this;  
    if (!events[event]) {
      events[event] = [];
    }

    events[event].push(fn);
  }

  EventEmitter.prototype.emit = function (event, ...args) {
    const { events } = this;
    const listeners = events[event];
    if (!listeners || listeners.length === 0) {
      return;
    }

    for (let i = 0; i < listeners.length; i++) {
      listeners[i].apply(this, args);
    }
  }

  那么 mini-signals 到底与大部分的发布-订阅模式的实现有什么区别呢?接下来本文带你一步步揭晓。

二、基础使用

  探索源码之前,首先需要了解 mini-signals 的使用方式:

  const signal = new MiniSignals();

  signal.add(task);
  signal.dispatch('foo''bar');

  function task(foo, bar) {
    // do something
  }

  而大部分同学习惯的使用方式如下:

  const events = new EventEitter();
  events.on('eventname', task);
  events.emit('eventname''foo''bar');

  单从使用方式来看,mini-signals 有如下优势:

  • 不需要通过额外的常量来管理发布-订阅的事件名。
  • 事件不再通过一条总线管理,一个实例对应一个独立的事件容器,一定程度上减少了程序的复杂度。

三、双向链表管理事件监听

  mini-signals 并没有直接在 MiniSignal 对象上创建一个数组来管理事件监听。

class MiniSignal {
  constructor () {
    // 前驱和后继
    this._head = this._tail = undefined;
  }
}

  而是选择实现一个朴素的双向链表来管理事件容器上的事件监听,由于双向链表的节点需要记录其前驱和后继节点,所以需要 MiniSignalBinding 类来创建节点实例:

class MiniSignalBinding {
  constructor (fn, once = false, thisArg) {
    this._fn = fn;
    // 回调函数是否只执行一次
    this._once = once;
    this._thisArg = thisArg;
    // 记录当前节点的前驱和后继
    this._next = this._prev = null;
    // 归属的事件容器
    this._owner = null;
  }
  // 解除绑定方法
  detach () {
    if (this._owner === nullreturn false;
    this._owner.detach(this);
    return true;
  }

}

  MiniSignalBinding 内部通过 _prev 和 _next 来记录前驱和后继节点,并且通过 _owner 属性来记录当前节点归属的事件容器。

  这一套操作还是比较专业的。

四、注册事件监听

  MiniSignal 提供 add 方法来注册事件监听:

  MiniSignal.prototype.add = function (fn, thisArg = null) {
    if (typeof fn !== 'function') {
      throw new Error('MiniSignal#add(): First arg must be a Function.');
    }
    return _addMiniSignalBinding(thisnew MiniSignalBinding(fn, false, thisArg));
  }

  创建好节点对象后,需要更新其前驱节点、后继节点以及归属的事件容器,同时还需要更新 MiniSignal 中保存的头节点和尾节点:

  function _addMiniSignalBinding (self, node) {
    if (!self._head) {
      self._head = node;
      self._tail = node;
    } else {
      self._tail._next = node;
      node._prev = self._tail;
      self._tail = node;
    }

    node._owner = self;

    return node;
  }

五、派发事件

  MiniSignal 派发事件时,需要实现遍历链表的操作,然后执行保存在节点中的回调函数即可:

  dispatch () {
    let node = this._head;

    if (!node) return false;

    while (node) {
      if (node._once) this.detach(node);
      node._fn.apply(node._thisArg, arguments);
      node = node._next;
    }

    return true;
  }

  另外对于只需要执行一次的事件监听,需要同时进行解绑操作。

六、解绑事件监听

  解绑操作需要实现链表的删除操作,更新前驱节点的后继指针和后继节点的前驱指针:

  detach (node) {
    if (!(node instanceof MiniSignalBinding)) {
      throw new Error('MiniSignal#detach(): First arg must be a MiniSignalBinding object.');
    }
    if (node._owner !== this) return this;

    if (node._prev) node._prev._next = node._next;
    if (node._next) node._next._prev = node._prev;

    if (node === this._head) {
      this._head = node._next;
      if (node._next === null) {
        this._tail = null;
      }
    } else if (node === this._tail) {
      this._tail = node._prev;
      this._tail._next = null;
    }

    node._owner = null;
    return this;
  }

七、总结

  以上便是 mini-signals 库的核心代码,非常的轻量。

  同时也可以看出 MiniSignal 相比较大部分的发布-订阅模式的实现还是有一些优势的:

  • 利用双向链表管理事件监听,从而引入了单独的对象来维护每一个事件监听函数,提供了程序的可读性。
  • 单独的事件容器,相比较事件名的约定,不需要再维护大量的常量事件名,一定程度上减少了程序的复杂度,提高了程序的可维护性。

  以上就是本文的全部内容,希望能够给你带来帮助,欢迎关注点赞转发