本文参加了由公众号@若川视野 发起的每周源码共读活动,点击了解详情一起参与。
【若川视野 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…
这个模式跟我们今天所看的库实现的方式是类似的,
源码分析
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!);
});
}
}
};
}
mitt和tiny-emitter的实现方式很类似,下面通过一个表格来显示两者的特点
| tiny-emitter | mitt | |
|---|---|---|
| 事件注册 | 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)来通知对应的回调函数执行。