动手造轮子系列(一):发布订阅

494 阅读4分钟

前言
  最近开始想写这个系列文章,一方面是自己刚开始学编程的时候,也花了比较多的时间去学习各种框架的API,但却不了解内部的运行原理,即使了解也是通过各种文章和博客泛泛的了解一下。但从后面的经历来看,想快速的掌握一个框架,尝试自己实现一个demo版,会有奇效。希望结合自己的描述,能让新人少走一些弯路。另一方面,一些同学对于 "看源码" 有着一点崇拜但抗拒的情绪。虽然大家都说可以从 0.x 版本看起,可以减少阅读成本。但其实对于一些同学门槛还是高了一点,这个系列期望能用更精简和可读的代码介绍各个轮子的原理。

接口设计

  这期我们要实现的轮子是一个发布订阅的事件库,对标Events。我写代码一般习惯写之前先想好输入和输出,以及写好的代码会怎么用。发布订阅这种模型里面,两个核心的API on 和 emit ,通常用法如下:

var ee = new EventEmitter();
ee.on('message', function (text) {
  console.log(text);
});
ee.emit('message', 'hello world 1');

订阅一类消息和触发它。除了这两个核心API,我们还会实现一些周边的便捷实用的API
• addEventListener(event, listener) 为指定的事件注册一个监听器
• emit(event) 触发指定事件
• on(event, listener) 和addEventListener一致,只是别名
• removeListener(event, listener): 移除指定事件的监听器
• removeAllListeners([event]):移除指定事件的所有监听回调
• once(event, listener): 和on类似,但只触发一次

第一版实现

  有了接口设计和使用方法,可以开始造轮子了。先初始化一下代码

class EventEmitter {
  on(event, listener) {
  }
  emit(event, params) {
  }
}
export default EventEmitter

   先写两个核心的API。我们要监听事件和触发事件,总要有个地方存吧,JS里面能存数据的结构无非是数组和对象,而我们的事件是有名字的,那就先用对象。在监听事件的时候存进去,触发的事件调用一下

class EventEmitter {
  listeners = {};
  on(event, listener) {
    this.listeners[event] = listener;
  }
  emit(event, params) {
    this.listeners[event](params);
  }
}

跑一下我们最开始提的使用方法,一行醒目的 hello world 打印了出来。一个最小化demo就已经完成了,总共9行代码

完善

  不知道看完上面代码的同学有没有一种 "我又能行了" 的感觉😂。在这个基础上我们继续完善一下。在实际使用过程中,不管是DOM还是Node.js都支持同一个事件有多个监听器,比如这样

var ee = new EventEmitter();
ee.on('message', function (text) {
  console.log('第一次监听')
  console.log(text);
});
ee.on('message', function (text) {
  console.log('第二次监听')
  console.log(text);
});
ee.emit('message', 'hello world 1');

  这个难不倒我们,上面只存了一个listener,支持存多个listener就好了。数据结构改一下,变成数组:

class EventEmitter {
  listeners = {};
  on(event, listener) {
    if (this.listeners[event]) {
      this.listeners[event].push(listener)
    } else {
      this.listeners[event] = [listener]
    }
  }
  emit(event, params) {
    if (!this.listeners[event]) return; 
    this.listeners[event].forEach((listener) => {
      listener(params)
    })
  }
}

触发事件时也调整为了依次调用监听器。

支持一下 addEventListener 方法:

addEventListener(event, listener) {
    this.on(event, listener);
}   

有了上面的思路,清除所有监听器也是水到渠成。直接delete

removeAllListeners(event) {
    if (!this.listeners[event]) return;
    delete this.listeners[event];
}

清除特定的 listener

removeListeners(event, listener) {
    if (!this.listeners[event]) return;
    const index = this.listeners[event].indexOf(listener);
    this.listeners[event].splice(index, 1)
 }

只触发一次的once: 触发过后就解除监听

once(event, listener) {
    let self = this
    this.on(event, function () {
      let args = Array.prototype.slice.call(arguments);
      listener.apply(null, args);
      self.removeListeners(event, listener)
    })
}

完整代码:源码地址

class EventEmitter {
  listeners = {};
  on(event, listener) {
    if (this.listeners[event]) {
      this.listeners[event].push(listener)
    } else {
      this.listeners[event] = [listener]
    }
  }
  emit(event, params?) {
    if (!this.listeners[event]) return;
    this.listeners[event].forEach((listener) => {
      listener(params)
    })
  }
  addEventListener(event, listener) {
    this.on(event, listener);
  }
  removeAllListeners(event) {
    if (!this.listeners[event]) return;
    delete this.listeners[event];
  }
  removeListeners(event, listener) {
    if (!this.listeners[event]) return;
    const index = this.listeners[event].indexOf(listener);
    this.listeners[event].splice(index, 1)
  }
  once(event, listener) {
    let self = this
    this.on(event, function () {
      let args = Array.prototype.slice.call(arguments);
      listener.apply(null, args);
      self.removeListeners(event, listener)
    })
  }
}
export default EventEmitter

小结

一个学习使用的小demo到这里就完成了,生产使用的源码会复杂得多,包含了各种各样的边际情况和异常处理,打印出开发易于阅读的堆栈提示。但其中的核心代码都是类似的