【源码共读】第25期 | mitt、tiny-emitter

584 阅读6分钟

1. 前言

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

这是源码共读的第25期,链接:mitt、tiny-emitter

mitt仓库 

tiny-emitter仓库 

2. mitt、tiny-emitter是什么

mitttiny-emitter 都是轻量级的库,用于在JavaScript环境中实现自定义事件的发布和订阅。

许多复杂的JavaScript应用要依赖于事件来在组件或模块之间通信。虽然浏览器自身提供了类似的功能,如DOM事件,但这些通常过于重量级,不适合在轻量级的或者没有DOM的环境(如 Node.js)中使用。此外,DOM事件也无法满足一些特殊需求,如全局事件、通配符事件等。

因此,出现了像mitttiny-emitter这样的轻量级库,它们解决了如何在任何JavaScript应用中轻松地创建、监听和触发自定义事件的问题。

两者都是使用了发布订阅模式,接下来解释下其原理。

3. 观察者&发布订阅

许多人说两者不是同一个模式,个人觉得,发布订阅是在观察者的基础上变化而来,没必要纠结。

下面用两个例子进行说明

3.1 观察者模式

比如老师需要通知学生明天上课,将所有学生拉到一个消息群中,老师发送消息,所有学生都收到消息。

image.png

代码实现:

/**
   * 1.被观察者拥有所有观察者的数组
   * 2.事件发布时遍历完整列表,通知每一个观察者
   * 老师在把学生全部拉到群里,发送消息,每个学生都接收到
   */

  // 观察者
  class Observer {
    constructor(name) {
      this.name = name;
    }
    receiveMsg(msg) {
      console.log(`我是:${this.name},收到了消息${msg}`);
    }
  }

  // 被观察者
  class Subject {
    constructor() {
      this.observers = [];
    }
    addObserver(ob) {
      this.observers.push(ob);
      return this;
    }

    removeObserver(observer) {
      let index = this.observers.indexOf(observer);
      if (index > -1) {
        this.observers.splice(index, 1);
      }
    }

    notify(msg) {
      this.observers.forEach((ele) => ele.receiveMsg(msg));
    }
  }

  const student1 = new Observer("jack");
  const student2 = new Observer("mary");
  const student3 = new Observer("alice");

  const teacher = new Subject();
  // 添加到群里
  teacher
    .addObserver(student1)
    .addObserver(student2)
    .addObserver(student3);
  // 发送消息
  teacher.notify(666);

3.2 发布订阅模式

某某学生说,我请假了,不想接受这么多消息。于是老师说,以后将消息发布在公众号,只有关注的才接受到消息。那么就需要一个发布订阅中心。

image.png

代码实现:

/**
   * 现在老师不直接拉群,新建一个公众号,公众号提供发布和订阅的方法,如果不订阅就收不到信息
   * 1. 订阅者提前订阅对应事件
   * 2. 事件发布时所有对应消息订阅回调全部执行
   */

  class Event {
    // 私有变量 所有消息的消息池
    handlers = {};
    // 订阅函数
    subscribe(msgName, handler) {
      if (!this.handlers[msgName]) {
        // 如果暂时没有
        this.handlers[msgName] = [];
      }
      // 如果有订阅了
      this.handlers[msgName].push(handler);
    }
    // 取消订阅
    unsubscribe(msgName, handler) {
      let handlerList = this.handlers[msgName];
      if (handlerList) {
        let index = handlerList.indexOf(handler);
        if (index > -1) {
          handlerList.splice(index, 1);
        }
      }
    }

    // 发布函数
    publish(msgName, ...data) {
      if (this.handlers[msgName]) {
        this.handlers[msgName].forEach((handler) =>
          handler.call(this, ...data)
        );
      }
    }
  }

  let event = new Event();

  event.subscribe("刘老师消息", (msg) => {
    console.log(`我是jack,收到了消息${msg}`);
  });
  event.subscribe("刘老师消息", (msg) => {
    console.log(`我是mary,收到了消息${msg}`);
  });
  event.subscribe("刘老师消息", (msg) => {
    console.log(`我是ace,收到了消息${msg}`);
  });

  setTimeout(() => {
    event.publish("刘老师消息", "888", "777");
  }, 1000);

3.3 小结

两者对比:

image.png

都是为了实现代码之间的解耦和消息通信,两者的区别:

  1. 观察者模式:一个或多个观察者对象直接监听一个主题对象。当主题对象状态改变时,它会通知所有的观察者对象。观察者和主题之间存在直接的依赖关系。所以,观察者模式通常用于一个对象需要通知其它对象,但又不需要知道这些对象具体是什么的情况。
  2. 发布-订阅模式:发布者和订阅者通过一个调度中心(也被称为“事件总线”、“消息队列”等)进行通信,发布者发布事件到调度中心,然后调度中心将这些事件派发给相关的订阅者。在发布-订阅模式中,发布者和订阅者是解耦的,它们不知道对方的存在。因此这种模式通常用于大型、复杂系统中,需要处理大量消息的发送和接收。

综上,观察者模式通常用于单一应用/过程内部,当一个对象的改变需要同时改变其它对象,而主对象并不知道具体有多少对象需要改变。而发布-订阅模式更多地应用在异步编程中,或者跨网络的消息传递。

3. 源码解读

通过上面的实现,看两个库的实现就比较简单了。

3.1 mitt

export default function mitt(all) {
    // 判断传入是否是所有的事件集合
    all = all || new Map();

    return {
        all,
        /** 订阅事件 */
        on(type, handler) {
            // 先获取查看是否已注册
            const handlers = all.get(type);
            if (handlers) {
                // 存在直接添加
                handlers.push(handler);
            } else {
                // 不存在,则新增
                all.set(type, [handler]);
            }
        },
        /** 销毁 */
        off(type, handler) {
            // 找出
            const handlers = all.get(type);
            if (handlers) {
                if (handler) {
                    // 有则删除回调  -1>>>0 = 4294967295  => [4294967295,1]
                    handlers.splice(handlers.indexOf(handler) >>> 0, 1);
                } else {
                    // 没有则至空
                    all.set(type, []);
                }
            }
        },
        /** 触发 */
        emit(type, evt) {
            // 回调回调列表
            let handlers = all.get(type);
            if (handlers) {
                handlers
                .slice()
                .map((handler) => {
                     handler(evt);
                });
            }
            //处理注册时用'*'作为事件类型的处理函数
            handlers = all.get('*');
            if (handlers) {
                handlers
                .slice()
                .map((handler) => {
                    handler(type, evt);
                });
            }
        }
    };
}


3.2 tiny-emitter

function E() {
}

E.prototype = {
    /** 订阅 */
    on: function (name, callback, ctx) {
        // 简写: 判断对象上是否存在e属性没有默认赋值空对象
        var e = this.e || (this.e = {});
        // 如果订阅的事件名称则push 不存在进行空数组赋值 再进行push保存 保存回调
        (e[name] || (e[name] = [])).push({
                fn: callback,
                ctx: ctx
        });
        // 返回this 为了链式调用
        return this;
    },


    /** 触发 */
    emit: function (name) {
        // 切割,提取出发布带入的入参,例如emit('msg',1,2,3)=> data为[1,2,3]
        var data = [].slice.call(arguments, 1);
        // 实际上是在检查this.e这个对象中是否有名为name的属性,如果有就返回其值(应该是数组)的副本,如果没有就返回一个空数组
        // 1. this.e || (this.e = {}):这是一个逻辑或操作。如果this.e已经定义,那么就直接使用this.e。否则,将创建一个新的空对象并赋值给this.e。
        // 2. [name]:这是在上一步结果(一个对象)中查找名为name的属性。
        // 3. (this.e[name] || []):这个是另一个逻辑或操作。如果找到的属性存在,就使用这个属性值,否则就使用一个新的空数组。
        // 4. slice():最后,调用数组的slice方法去创建原数组的浅拷贝。
        var evtArr = ((this.e || (this.e = {}))[name] || []).slice();
        // 遍历事件为name的回调
        var i = 0;
        var len = evtArr.length;

        for (i; i < len; i++) {
            // 触发回调,指定上下文和参数
            evtArr[i].fn.apply(evtArr[i].ctx, data);
        }
        // 链式调用
        return this;
    },
    /** 销毁 */
    off: function (name, callback) {
        // 简写: 判断对象上是否存在e属性没有默认赋值空对象
        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++) {
                // 只有当 evts[i].fn 不等于 callback 且 evts[i].fn._ 也不等于 callback时 才加入liveEvents
                // fn._是标记是否只调用一次的
                if (evts[i].fn !== callback && evts[i].fn._ !== callback) liveEvents.push(evts[i])
            }
        }

        // 如果有回调就进行赋值 没有就删除掉事件
        liveEvents.length ? (e[name] = liveEvents) : delete e[name]
        // 链式调用
        return this
    },

    /** 调用一次 */
    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);
    },
};

3.3 小结

相同点 不同点
mitt 1. 轻量级
2. API 设计:都提供了 `on`、`off`、`emit`
3. 运行在浏览器和 Node.js 中
1. 支持全局('*')事件监听
2. 获取所有事件
tiny-emitter 1. 支持设置执行上下文This,支持链式调用
2. 支持多参数多类型传参

4. 总结

通过本次源码阅读,学习了:

  1. 观察者模式和发布订阅的区别
  2. 了解了mitt和tiny-emiiter的实现原理

一起学习,如果错误,请指正O^O!

参考文章:

第8期:mitt、tiny-emitter 发布订阅