likes
comments
collection
share

基于mitt、tiny-emitter 的源码发布/订阅模式解析

作者站长头像
站长
· 阅读数 73

什么是发布订阅模式

此模式分为发布者和订阅者两个概念,发布者收集订阅者的需求,然后在某个时刻告知订阅者。 发布订阅模式中,有三种主体:1. publisher (发布者) 2. subscriber (订阅者)3. broker(经纪人或者叫调度中心)。还有一个概念叫做topic(话题或者叫做订阅的主题)

发布-订阅模式其实是一种对象间一对多的依赖关系,当一个对象的状态发送改变时,所有依赖于它的对象都将得到状态改变的通知。 订阅者(Subscriber)把自己想订阅的事件(topic)注册(Subscribe)到调度中心(Event Channel或broker),当发布者(Publisher)发布该事件(topic)到调度中心,也就是该事件触发时,由调度中心统一调度订阅者注册到调度中心的处理代码。

和发布订阅模式最容易混淆的是观察者模式,如果不是特别清楚的话,建议看一下二者的对比,加深一下理解。 【JS设计模式】观察者模式VS发布订阅模式以及发布订阅模式

从上面了解了什么是发布订阅模式,那么它的作用就很明显的知道了,就是用于监听和触发事件执行不同的操作。

mitt源码解析

先看一下mitt这个库是怎么使用的:

import mitt from 'mitt'
const emitter = mitt()

emitter.on('foo', e => console.log('foo', e) )

emitter.on('*', (type, e) => console.log(type, e) )

emitter.emit('foo', { a: 'b' })

emitter.all.clear()

function onFoo() {}
emitter.on('foo', onFoo)
emitter.off('foo', onFoo)

on是用来注册监听的,emit是触发监听,off是移除监听,all 参数用来存储事件类型和事件处理函数的映射Map,如果不传,就 new Map()赋值给 all。

代码是ts的看起来我认为比较难受,所以我改成了js的:

export default function mitt(all) {
    all = all || new Map();
    
    return  {
        all,
        on (type, handler) {
            // 查找 map中是否有这个key
            const handlers = all.get(type);
            // 存在就直接push
            if (handlers) {
		handlers.push(handler);
	    } else {
                // 不存在就创建 value是一个数组
		all.set(type, [handler]);
	    }
        },
        off (type, handler) {
            const handlers = all.get(type);
            if (handlers) {
                if (handler) {
                    // 删除指定的 handler处理函数, 找到了 idx >>> 0 就是idx对应的索引
                    // 没找到 -1 变为 4294967295,原数组不会改变
                    handlers.splice(handlers.indexOf(handler) >>> 0, 1);
                } else {
                    // 如果没有传对应的handler,则把此type的全部事件全部清除
                    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); 
                }); 
             }
        }
    }
}

这一块代码我改造完成后最疑惑的就是handlers.splice(handlers.indexOf(handler) >>> 0, 1);这里为什么要使用右移运算符,我试过之后的大概想法是,如果出现的是-1这个值,如果不进行处理的话,那么就会删除数组最后一个函数,这个是有问题的,所以对它进行处理,这样就可以避免删除不该删除的函数,如果不是这个意思,麻烦大哥们在评论区告诉我原因。当然还有一个问题就是关于all这个变量,一定要设置为map类型,因为下面取值的时候都用的是set与get,否则将会报错。

tiny-emitter源码解析

tiny-emitter的原理和mitt其实是一样的,但是功能上有些许差异,并且实现上也有一些区别,大致的结构如下:

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;

tiny-emitter是针对原型链上或者说是基于class实现的,这种方式是原型模式,而mitt是基于返回一个对象的方式实现的,这种方式是工厂模式。

分别来看一下这些函数吧:

// on函数
on: function (name, callback, ctx) {
    // 创建变量e 指向this的属性 不存在则创建
    var e = this.e || (this.e = {});
    // 将对应的函数存放到对应的事件中,也是一个数组的形式
    (e[name] || (e[name] = [])).push({
      fn: callback,
      ctx: ctx
    });

    return this;
  },
// once函数
once: function (name, callback, ctx) {
    var self = this;
    // 实现once 的大前提是我们需要传入命名函数,存在引用关系可以让我们移除
    function listener () {
      // 移除对应的函数
      self.off(name, listener);
      // 调用一次 once传入的callback
      callback.apply(ctx, arguments);
    };
    // 存放引用,用于移除
    listener._ = callback
    // 调用on监听事件
    return this.on(name, listener, ctx);
  },
// emit函数
emit: function (name) {
    // 取arguments除第一个参数后面的参数
    var data = [].slice.call(arguments, 1);
    // 获取对应name的执行函数数组
    var evtArr = ((this.e || (this.e = {}))[name] || []).slice();
    var i = 0;
    var len = evtArr.length;

    for (i; i < len; i++) {
      // 依次将每个值(对象) 的fn属性的事件 通过apply调用 并传入 this指向 以及data剩余参数
      evtArr[i].fn.apply(evtArr[i].ctx, data);
    }

    return this;
  },
// off函数
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 存在内容则 给 e[name]重新赋值, 否则直接移除 这个name属性
    (liveEvents.length)
      ? e[name] = liveEvents
      : delete e[name];

    return this;
  }

总结

看了上面两个库对于发布/订阅的不同实现其实还是学到了很多东西,包括对于原型的使用,对于发布/订阅概念的理解,还有上面对于同一件事情实现的不同,对于我开阔眼界有一些帮助。

转载自:https://juejin.cn/post/7172124277985001486
评论
请登录