源码学习——tiny-emitter 发布订阅

46 阅读4分钟

“开启掘金成长之旅!这是我参与「掘金日新计划 · 2 月更文挑战」的第1天,点击查看活动详情

本文参加了由公众号@若川视野 发起的每周源码共读活动,点击了解详情一起参与。

这是源码共读的第8期,链接:juejin.cn/post/708498…

前言

发布订阅这个模式无论是在一些框架和库中还是在我们具体的业务开发中都会经常使用到。例如vue中的$on$emit方法,或者在业务中跨模块通信等场景都会使用到,以及原生的DOM事件/自定义事件。帮助我们更好的完成异步编程中的解耦。本篇文章我们就来深入学习下发布订阅模式及其如何实现。

什么是发布-订阅模式

发布订阅模式是一种对象间一对多的依赖关系。当一个对象的状态发生改变时,所有依赖它的对象都将得到状态改变的通知。

订阅者先把自己想订阅的事件注册到消息中心(Event Bus),当发布者发布该事件到消息中心(也就是事件触发时),则会由消息中心统一执行订阅者注册到消息中心的代码。

image.png

  • 消息中心:负责存储消息与订阅者的对应关系,有消息触发式,负责通知订阅者。
  • 订阅者:去消息中心订阅自己需要的消息。
  • 发布者:满足条件时,通过消息中心发布消息。

实现一个简易版发布订阅功能

const Event = {
    listenEvents: {},
    on: function(key, fn) {
        if(!this.listenEvents[key]) {
            this.listenEvents[key] = [];
        }
        this.listenEvents[key].push(fn); // 讲订阅的消息添加进缓存的列表
    },
    emit: function(key) {
        const fns = this.listenEvents[key];
        if(!fns || fns.length === 0) {
            return false;
        }
        fns.forEach(fn => {
            fn.apply(this, arguments);
        })
    }

};

Event.on('test1', (res) => {
    console.log('=====test1', res);
})
Event.on('test1', (res) => {
    console.log('=======test1 2', res);
})

Event.on('test2', (res) => {
    console.log('======test2', res);
})
Event.emit('test1', 1, 2, 3);
Event.emit('test2', 456);

通过上面的代码。我们已经简单的实现了事件的订阅和发布。 在有些场景下,我们需要具备取消订阅的能力。接下来我们就来实现下如何取消订阅。

Event.remove = function(key, fn) {
    const fns = this.listenEvents[key];
    if(!fns) {
        return false;
    }
    if(!fn) { // 如果没有传入具体的回调函数,则表示全部清空
        fns && (fns.length = 0);
    } else {
        const index = fns.findIndex(item => item === fn);
        if(index >= 0) {
            fns.splice(index, 1);
        }
    }
}

通过上面简单的代码实现,我们了解了发布订阅模式是如何运作的,接下来,我们来看下开源库tiny-emitter的实现。

tiny-emitter源码解读

源码地址:github.com/scottcorgan…

先看下其提供的API有哪些?

  • on: 订阅一个事件
  • once: 仅订阅一次事件
  • off: 解除绑定
  • emit: 发布事件 发布订阅的实现,提供的API都差不多,我们直接来看其是如何实现。
 function E() {}
 E.prototype = {
    on: function (name, callback, ctx) { },
  
    once: function (name, callback, ctx) { },
  
    emit: function (name) { },
  
    off: function (name, callback) { }
  };
  
  module.exports = E;
  module.exports.TinyEmitter = E;

看其实现是基于原型链来实现的。

下面来学习各个方法的具体实现:

on方法

function (name, callback, ctx) {
      var e = this.e || (this.e = {});
  
      (e[name] || (e[name] = [])).push({
        fn: callback,
        ctx: ctx
      });
  
      return this;
    },
  • 创建一个对象,有则直接使用,没有的话新创建一个,用来存储注册的事件及其对应的执行方法
  • 将传进来的事件名称及事件回调存入到对象中。

emit方法

function (name) {
      var data = [].slice.call(arguments, 1);
      var evtArr = ((this.e || (this.e = {}))[name] || []).slice();
      var i = 0;
      var len = evtArr.length;
  
      for (i; i < len; i++) {
        evtArr[i].fn.apply(evtArr[i].ctx, data);
      }
      return this;
    },
  • 获取arguments参数,传入注册的回调函数中。
  • 获取对应name存入的回调函数数组,遍历执行,并将传进来的数据传递进去。

off方法

function (name, callback) {
      var e = this.e || (this.e = {});
      var evts = e[name];
      var liveEvents = [];
  
      if (evts && callback) {
        for (var i = 0, len = evts.length; i < len; i++) {
          if (evts[i].fn !== callback && evts[i].fn._ !== callback)
            liveEvents.push(evts[i]);
        }
      }
      (liveEvents.length)
        ? e[name] = liveEvents
        : delete e[name];
  
      return this;
    }
  • 先取出name对应的回调函数数组。
  • 遍历该数组,跟传入的回调函数进行比较,不相等的函数存入liveEvents。
  • liveEvents有数据的话,重新赋值对应的name,否则直接删除name对应的数据。

once方法

function (name, callback, ctx) {
      var self = this;
      function listener () {
        self.off(name, listener);
        callback.apply(ctx, arguments);
      };
  
      listener._ = callback
      return this.on(name, listener, ctx);
    },
  • 这个once方法值得研究下,这里内部封装了一个函数,用来处理只订阅一个事件。
  • 在这个函数里先注销对应的函数,然后执行传入进来的函数,这里用到了闭包。
  • 然后调用on方法来订阅事件,传入的回调函数就是内部封装的这个函数。
  • once的理解就是订阅相同name的事件时,不论订阅多少次,只会执行最后一次订阅时,传入的回调函数。

总结

发布订阅模式作为常用的一种模式,在很多业务场景帮助我们更好的解决问题,有其优点但也有对应的问题。

  • 优点:一个时间上的解耦,另一个是对象之间的解耦
  • 缺点: 如果模块之间用了太多的全局的发布订阅模式通信,那么模块与模块之间的联系就被隐藏到背后,会搞不清消息来自哪个模块,或者消息流向哪些模块,这就造成我们的维护成本大增。