【源码共读】 | mitt、tiny-emitter 发布订阅

218 阅读4分钟

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

【若川视野 x 源码共读】第8期 | mitt、tiny-emitter 发布订阅 点击了解本期详情一起参与

我们来看这段代码

   const vm = new Vue();
   
   vm.$on('test', function (msg) {
     console.log(msg)
   })
   vm.$emit('test', 'Hello Juejin')

上述代码中使用了$on$emit进行通信,熟悉vue的小伙伴知道,这是一个常用的组件间通信方式,子组件用过$emit来触发父组件的事件执行。那么,这里面的原理是怎么样的呢。

发布订阅模式

根据官方文档,我们得知,vue采用的是发布订阅的模式

文档地址:cn.vuejs.org/guide/compo…

image-20220928083830520

这个模式跟我们今天所看的库实现的方式是类似的,

源码分析

tiny-emitter

源码地址:

首先,我们先看tiny-emitter

/* ...前面省略 */
function E () {
 // 留空,更好的实现继承,动态创建事件集
 // https://github.com/scottcorgan/tiny-emitter/issues/3
}

E.prototype = {
  on: function (name, callback, ctx) {
    // 获取事件映射表,如果不存在,则赋值为空对象
    var e = this.e || (this.e = {});

    (e[name] || (e[name] = [])).push({
      fn: callback,
      ctx: ctx
    });

    return this;
  },

  once: function (name, callback, ctx) {
    // 保存当前的this
    var self = this;
    function listener () {
      // 执行后移除自身
      self.off(name, listener);
      // 调用传入的callback
      callback.apply(ctx, arguments);
    };

    // 订阅事件时,修改了this引用,所以找不到对应的listener
    // 需要将当前的callback绑定
    listener._ = callback
    return this.on(name, listener, ctx);
  },

  emit: function (name) {
    // 截取 name 之后的所有参数,返回一个新的数组
    // var data = [...args]
    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;
  },

  off: function (name, callback) {
    // 如果e不存在则new Map()
    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]重新赋值
    (liveEvents.length)
      ? e[name] = liveEvents
      : delete e[name];

    return this;
  }
};

/* ...后面省略  */

整个流程下来比较简单,就是有一个Map来收集事件,通过on来对事件进行注册回调函数,当调用emit时去Map中查找对应的事件组并依次执行。

  • off注销事件注册
  • once执行了一次调用后,注销事件

mitt

/* ...省略类型声明  */
export default function mitt<Events extends Record<EventType, unknown>>(
	all?: EventHandlerMap<Events>
): Emitter<Events> {
	type GenericEventHandler =
		| Handler<Events[keyof Events]>
		| WildcardHandler<Events>;
	all = all || new Map();

	return {

		/**
		 * 注册所有订阅方法映射表
		 */
		all,

		/**
		 * Register an event handler for the given type.
		 * @参数 {string|symbol} 声明需要监听的事件名称,或者输入'*'通配符来监听所有事件
		 * @参数 {Function} 传入回调函数,当触发事件时调用
		 * @ mitt 中的方法
		 */
		on<Key extends keyof Events>(type: Key, handler: GenericEventHandler) {
			// 获取对应的回调函数数组
			const handlers: Array<GenericEventHandler> | undefined = all!.get(type);
			if (handlers) {
				handlers.push(handler);
			}
			else {
				all!.set(type, [handler] as EventHandlerList<Events[keyof Events]>);
			}
		},

		/**
		 * 根据给出的名字,移除调度中心中对应的注册事件
		 * 如果省略 handler,则移除所有注册事件
		 * @参数 {string|symbol} 输入需要移除的注册事件名称,如果输入通配符'*'则移除全部注册事件
		 * @参数 {Function} 需要删除的回调函数
		 * @ mitt 中的方法
		 */
		off<Key extends keyof Events>(type: Key, handler?: GenericEventHandler) {
			const handlers: Array<GenericEventHandler> | undefined = all!.get(type);
			if (handlers) {
				if (handler) {
					// 右移操作符,如果找不到,则变成  handlers.splice(4294967295,1)
					// 对原有数组不影响
					handlers.splice(handlers.indexOf(handler) >>> 0, 1);
				}
				else {
					// 如果没有handler,重置all
					all!.set(type, []);
				}
			}
		},

		/**
		 * 根据传入的注册事件名,调用对应的回调函数组
		 * 如果输入的是通配符'*',则触发所有的注册事件
		 *
		 * 注意,不支持手动触发'*'来触发所有的注册事件
		 *
		 * @参数 {string|symbol} 参数需要触发的注册事件名称
		 * @参数 {Any} [evt] Any value (如果输入多个参数,需要先用对象进行封装),传参给回调函数
		 * @ mitt 中的方法
		 */
           // 传参时,如果是多个参数,需要自己做处理,不能使用(xxx,xxx,xxx...)
		emit<Key extends keyof Events>(type: Key, evt?: Events[Key]) {
			let handlers = all!.get(type);
			if (handlers) {
				// 切割数组,依次执行callback
				(handlers as EventHandlerList<Events[keyof Events]>)
					.slice()
					.map((handler) => {
						handler(evt!);
					});
			}
			// 处理通配符事件
			handlers = all!.get('*');
			if (handlers) {
				// 切割数组,依次执行callback
				(handlers as WildCardEventHandlerList<Events>)
					.slice()
					.map((handler) => {
						handler(type, evt!);
					});
			}
		}
	};
}

mitttiny-emitter的实现方式很类似,下面通过一个表格来显示两者的特点

tiny-emittermitt
事件注册on: function (name, callback, ctx)on(type: Key, handler: GenericEventHandler)
事件注销off: function (name, callback)off(type: Key, handler?: GenericEventHandler)
派发事件emit: function (name)emit(type: Key, evt?: Events[Key])
一次分发 once: function (name, callback, ctx)
支持通配符 * 监听事件

总结

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

在这个对象中,有一个map来充当调度中心,通过on将回调函数注册到调度中心上,通过emit(name)来通知对应的回调函数执行。


参考文章

github.com/xiong-ling/…

juejin.cn/post/684490…