mitt源码深度剖析
大家好,我是晴天同学,这是我的第一篇博客,有什么写的不对的地方,欢迎大家在评论区指正!
介绍
mitt是一个微型(~200b)的订阅与发布模式的库,简单的100多行代码(包括类型与注释)实现了事件的订阅(on)、发布(emit)、取消订阅(off),并支持vue和react,下面是它的API:
- all:用于存储所有事件的Map结构
- on:用于给指定类型注册处理事件
- off:删除给定类型的事件处理程序。如果
handler
省略,则删除给定类型的所有处理程序。
- emit:调用给定类型的所有处理程序。如果存在,
'*'
则在类型匹配的处理程序之后调用处理程序。 注意:不支持手动触发"*"处理程序。
下面我们学习一下它的原理。
原理
通过上述图表可知,订阅者通过调用on方法注册指定类型的事件到调度中心all中,发布者通过调用emit方法调用给定type的处理程序,这样该type的订阅者就可以响应到emit发送的数据,从而完成整个发布订阅的流程。
源码实现
源码结构
通过上面分析,我们大致可以分为以下结构:
js
function mitt() {
// all用于存储所有事件
const all = new Map();
// on方法用于注册事件
const on = (type, handler) => {
};
// off方法用于移除事件
const off = (type, handler) => {
};
// emit方法用于触发事件
const emit = (type, params) => {
};
return { on, off, emit };
}
all
all是用来存储所有类型的事件的Map结构,有些时候我们需要去接受其他mitt实例的Map用来初始化我们自己的Map,如果不传入这个Map我们就默认为一个空Map。
js
function mitt(all) {
// all用于存储所有事件
all = all || new Map();
...
}
如果我们想清空所有type也非常简单,直接调用Map的clear方法即可。
js
const emitter=mitt();
emitter.all.clear();
on
通过上述流程图可以看到,当我们调用on方法监听指定type时,我们需要先判断all中有没有存储该type,如果存储了该type,我们就将事件handler给push到该type事里面,如果不存在该type,我们将创建一个该type和一个只包含该handler的数组
js
function mitt(all) {
...
// on方法用于注册事件
const on = (type, handler) => {
const handlers = all.get(type);
if (handlers) {
handlers.push(handler);
} else {
all.set(type, [handler]);
}
};
...
}
off
通过上述流程图可以看到,当我们调用off移除指定type事件时,我们需要先判断off方法有无传指定handler事件,如果没有传我们就移除该type对应的所有事件,如果传了handler事件,我们会先判断该type对应的handler数组有没有该handler,如果有,就移除该handler。
js
function mitt(all) {
...
// off方法用于移除事件
const off = (type, handler) => {
const handlers = all.get(type);
if (handlers) {
if (handler) {
const index = handlers.indexOf(handler);
if (index !== -1) {
handlers.splice(index, 1);
}
} else {
all.set(type, []);
}
}
};
...
}
注意:源码和上述代码的不同之处
js
// 我们的代码
if (index !== -1) {
handlers.splice(index, 1);
}
// 源码
handlers.splice(handlers.indexOf(handler) >>> 0, 1);
// -1的二进制 11111111 11111111 11111111 11111111
// 无符号右移0位后 01111111 11111111 11111111 11111111
源码并没有判断该handler是否存在于数组中,也就是说我们handlers.indexOf(handler)
存在为-1的可能,而handlers.splice
的第一个参数不能为负数。源码巧妙的采用 无符号右移 的方式规避了这个问题。无符号右移会将我们的十进制数字转化成二进制来进行右移计算,正整数右移0位还是该正整数,负整数右移0位相当于二进制的符号改为0其余不变。由上述代码片段可知-1无符号右移0位是2的32次方-1,是一个非常大的数,并不会修改handler数组。关于无符号右移具体请参考:MDN无符号右移
emit
通过上述流程图可以看到,当我们使用emit函数时,会先判断all中type对应的事件数组存不存在,如果存在我们会将该事件数组里的事件依次取出执行。然后我们会再判断all中存不存在通配符*
,如果存在通配符*
对应的事件数组,我们会把通配符*
对应的事件数组依次取出执行,这时我们执行数组的参数也有了些许变化,会附带上对应的type。
js
function mitt(all) {
...
// emit方法用于触发事件
const emit = (type, evt) => {
let handlers = all.get(type);
if (handlers) {
// handlers.forEach((handler) => handler(evt));
handlers.slice().map((handler) => handler(evt));
}
// 判断是否监听了所有的事件
handlers = all.get('*');
if (handlers) {
// handlers.forEach((handler) => handler(type, evt));
handlers.slice().map((handler) => handler(type, evt));
}
}
...
}
注意:源码中为何使用.slice().map()
的方式执行事件数组而不是用.forEach()
的方式执行事件数组?
回答:这里使用 .slice().map()
的原因是为了创建事件数组的一个副本,然后在这个副本上进行迭代。这样做的好处是,即使在事件的执行过程中修改了原始数组(例如,通过添加或删除处理程序),也不会影响当前正在进行的迭代。相比之下,如果你直接在原始数组上使用 .forEach()
,那么在迭代过程中对数组的任何修改都可能导致未定义的行为。例如,如果你在处理一个事件的过程中删除了一个处理程序,那么 .forEach()
可能会跳过一些处理程序,因为它们的索引已经改变了。
完整源码
js
function mitt(all) {
// all用于存储所有事件
all = all || new Map();
// on方法用于注册事件
const on = (type, handler) => {
const handlers = all.get(type);
if (handlers) {
handlers.push(handler);
} else {
all.set(type, [handler]);
}
};
// off方法用于移除事件
const off = (type, handler) => {
const handlers = all.get(type);
if (handlers) {
if (handler) {
const index = handlers.indexOf(handler);
if (index !== -1) {
handlers.splice(index, 1);
}
} else {
all.set(type, []);
}
}
};
// emit方法用于触发事件
const 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));
}
};
return { all, on, off, emit };
}
拓展:增加once方法
once方法的实现原理:当我们第一次执行on方法的时候就在该type的handler事件处理函数中去off该handler。如下所示:我们创建一个_handler函数去执行once中传入的handler事件处理函数,之后立刻卸载(off)掉_handler函数。
js
function mitt(all) {
...
// 监听一次事件
const once = (type, handler) => {
const _handler = (...args) => {
handler(...args);
off(type, _handler);
};
on(type, _handler);
};
...
}
增加TypeScript类型标注
类型结构
通过上述的源码可知,我们如果想给mitt()
函数增加类型,并可以正确的推导类型结构和参数类型,就必须需要一个泛型Events
,Events
是一个key为type
,value为handler事件参数
类型的对象。mitt()
就是一个接收Events
泛型并返回一个Emitter<Events>
类型的函数。我们可以得到以下类型结构:
ts
// 约束事件类型:string | symbol
type EventType = string | symbol;
// mitt函数的返回值类型
interface Emitter<Events extends Record<EventType, unknown>> {
all: any;
on: any;
off: any;
emit: any;
}
function mitt<Events extends Record<EventType, unknown>>(all:any): Emitter<Events> {
...
}
Events extends Record<EventType, unknown>
用来约束Events
类型必须为key是type
,value是handler事件参数
的类型的对象。接下来我们的任务就变成了如何去完善Emitter
类型。
all
all是一个key为type
或*
,value是handler事件处理函数的数组的Map结构,我们可以得到以下代码:
ts
// 事件类型和对应事件处理程序的映射表
type EventHandlerMap<Events extends Record<EventType, unknown>> = Map<
keyof Events|'*',
V?
>;
// mitt函数的返回值类型
interface Emitter<Events extends Record<EventType, unknown>> {
all: EventHandlerMap<Events>;
...
}
Map的V
类型我们可以根据keyof Events|'*'
分成两种情况去看:
-
正常type
keyof Events
如果是传入正常的type,那么我们需要给Map的类型
V
传入一个泛型,将Events
的type对应的value作为泛型传给类型V
,可以得到以下类型:Array<(event: Events[keyof Events]) => void>
,简单整理一下得出以下类型:
ts
// 一个事件处理程序可以接受一个可选的事件参数
// 并且不应该返回任何值
type Handler<T = unknown> = (event: T) => void;
// 当前注册的某个类型的所有事件处理程序的数组
type EventHandlerList<T = unknown> = Array<Handler<T>>;
// 事件类型和对应事件处理程序的映射表
type EventHandlerMap<Events extends Record<EventType, unknown>> = Map<
keyof Events,
EventHandlerList<Events[keyof Events]>
>;
- 通配符
*
如果传入的是通配符*
,我们的事件处理函数有两个参数,一个是type,一个是handler函数,不难看出通配符类型是需要接受Events
类型来做参数,并在函数中根据传入的类型去推导对应的type和handler类型,可以得到以下类型Array<(type: keyof Events, event: Events[keyof Events]) => void>
,简单整理一下得出以下类型:
ts
// 一个事件处理程序可以接受一个可选的事件参数
// 并且不应该返回任何值
type WildcardHandler<T = Record<string, unknown>> = (type: keyof T, event: T[keyof T]) => void;
// 当前注册的某个类型的所有事件处理程序的数组
type WildCardEventHandlerList<T = Record<string, unknown>> = Array<WildcardHandler<T>>;
// 事件类型和对应事件处理程序的映射表
type EventHandlerMap<Events extends Record<EventType, unknown>> = Map<
'*',
WildCardEventHandlerList<Events>
>;
将上面的两种情况合并可以得出all的类型:
ts
// 约束事件类型:string | symbol
type EventType = string | symbol;
// 一个事件处理程序可以接受一个可选的事件参数
// 并且不应该返回任何值
type Handler<T = unknown> = (event: T) => void;
type WildcardHandler<T = Record<string, unknown>> = (type: keyof T, event: T[keyof T]) => void;
// 当前注册的某个类型的所有事件处理程序的数组
type EventHandlerList<T = unknown> = Array<Handler<T>>;
type WildCardEventHandlerList<T = Record<string, unknown>> = Array<WildcardHandler<T>>;
// 事件类型和对应事件处理程序的映射表
type EventHandlerMap<Events extends Record<EventType, unknown>> = Map<
keyof Events | '*',
EventHandlerList<Events[keyof Events]> | WildCardEventHandlerList<Events>
>;
// mitt函数的返回值类型
interface Emitter<Events extends Record<EventType, unknown>> {
all: EventHandlerMap<Events>;
...
}
on&off
on&off函数的参数是type和handler,也分为两种情况,一种是正常type,一种是通配符 *
,而handler的类型就是传入Array的泛型,根据上述all的类型标注不难推出on和off的类型:
ts
...
// mitt函数的返回值类型
interface Emitter<Events extends Record<EventType, unknown>> {
all: EventHandlerMap<Events>;
// 注册一个指定类型的事件处理程序
on<Key extends keyof Events>(type: Key, handler: Handler<Events[Key]>): void;
// 注册一个通配符类型的事件处理程序
on(type: '*', handler: WildcardHandler<Events>): void;
// 取消注册一个指定类型的事件处理程序
off<Key extends keyof Events>(type: Key, handler?: Handler<Events[Key]>): void;
// 取消注册一个通配符类型的事件处理程序
off(type: '*', handler: WildcardHandler<Events>): void;
emit: any;
}
emit
emit
函数的类型标注就非常简单了,emit函数的两个参数type、event就是Events
的key和value,这样我们可以得到下面的类型:
ts
// mitt函数的返回值类型
interface Emitter<Events extends Record<EventType, unknown>> {
// 触发指定类型的事件
emit<Key extends keyof Events>(type: Key, event: Events[Key]): void;
// 防止用户使用emit()当作emit('*')来使用
emit<Key extends keyof Events>(type: undefined extends Events[Key] ? Key : never): void;
}
完整代码
ts
export type EventType = string | symbol;
// 一个事件处理程序可以接受一个可选的事件参数
// 并且不应该返回任何值
export type Handler<T = unknown> = (event: T) => void;
export type WildcardHandler<T = Record<string, unknown>> = (
type: keyof T,
event: T[keyof T]
) => void;
// 当前注册的某个类型的所有事件处理程序的数组
export type EventHandlerList<T = unknown> = Array<Handler<T>>;
export type WildCardEventHandlerList<T = Record<string, unknown>> = Array<WildcardHandler<T>>;
// 事件类型和对应事件处理程序的映射表
export type EventHandlerMap<Events extends Record<EventType, unknown>> = Map<
keyof Events | '*',
EventHandlerList<Events[keyof Events]> | WildCardEventHandlerList<Events>
>;
export interface Emitter<Events extends Record<EventType, unknown>> {
all: EventHandlerMap<Events>;
// 注册一个指定类型的事件处理程序
on<Key extends keyof Events>(type: Key, handler: Handler<Events[Key]>): void;
// 注册一个通配符类型的事件处理程序
on(type: '*', handler: WildcardHandler<Events>): void;
// 取消注册一个指定类型的事件处理程序
off<Key extends keyof Events>(type: Key, handler?: Handler<Events[Key]>): void;
// 取消注册一个通配符类型的事件处理程序
off(type: '*', handler: WildcardHandler<Events>): void;
// 触发指定类型的事件
emit<Key extends keyof Events>(type: Key, event: Events[Key]): void;
// 触发通配符类型的事件
emit<Key extends keyof Events>(type: undefined extends Events[Key] ? Key : never): void;
}
/**
* Mitt: 轻量级(约200字节)的函数式事件发射器/发布-订阅模式。
* @name mitt
* @returns {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,
/**
* 注册一个指定类型的事件处理程序
* @param {string|symbol} type 要监听的事件类型,或者使用 `'*'` 监听所有事件
* @param {Function} handler 响应事件的处理函数
* @memberOf 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` 参数,则会移除该类型的所有处理程序
* @param {string|symbol} type 要取消注册 `handler` 的事件类型(使用 `'*'` 移除通配符处理程序)
* @param {Function} [handler] 要移除的处理函数
* @memberOf mitt
*/
off<Key extends keyof Events>(type: Key, handler?: GenericEventHandler) {
const handlers: Array<GenericEventHandler> | undefined = all!.get(type);
if (handlers) {
if (handler) {
handlers.splice(handlers.indexOf(handler) >>> 0, 1);
} else {
all!.set(type, []);
}
}
},
/**
* 触发指定类型的所有事件处理程序
* 如果存在,`'*'` 类型的处理程序会在类型匹配的处理程序之后被触发
*
* 注意:不支持手动触发 `'*'` 类型的处理程序
*
* @param {string|symbol} type 要触发的事件类型
* @param {Any} [evt] 传递给每个处理程序的任意值(推荐使用对象,更加强大)
* @memberOf mitt
*/
emit<Key extends keyof Events>(type: Key, evt?: Events[Key]) {
let handlers = all!.get(type);
if (handlers) {
(handlers as EventHandlerList<Events[keyof Events]>).slice().map((handler) => {
handler(evt!);
});
}
handlers = all!.get('*');
if (handlers) {
(handlers as WildCardEventHandlerList<Events>).slice().map((handler) => {
handler(type, evt!);
});
}
},
};
}