Mitt源码解读
github地址:https://github.com/developit/mitt
200b大小的event bus库
类型系统
让人眼前一亮的是,该库对类型有着较强规范,用户可以很方便的用下面的方式定义事件类型,在该issus被提出
1 | type Events = { |
那它是如何做到的呢,先把实现代码展示出来再分部讲解(直接看懂的可以跳过讲解)。
1 | export type EventType = string | symbol; |
Record
Record<K, T>
是typescript内置的类型函数
1 | // Construct a type with a set of properties K of type T |
代表的意思是从第一个参数获取所有键,值为第二个参数,源码中Record<EventType, unknown>
即为
1 | export type EventType = string | symbol; |
Events extends Record<EventType, unknown>
利用泛型约束将用户传入的Events类型约束为上述类型
类型编程
就像余华老师睡一觉醒来后有了这么一个题目叫《活着》,觉得题目非常好就开始写了,第一次见到Linbudu的博客,见到了一个词——类型编程,有了这么一个概念(虽然不知道是在何时何地被提出的),突然很多知识就有了一个系统的名称,引导人们探索这个系统下的各种应用,下面就是一个很简单的一个例子。
1 | export type Handler<T = unknown> = (event: T) => void; |
Events[keyof Events]
就是将用户定义的Events类型的值类型全部取出,在最上面的使用方式的例子里,会有如下的转化结果。
1 | type Events = { |
再看on
、off
、emit
函数定义,去除了 支持type为*
的功能
1 | on<Key extends keyof Events> (type: Key, handler: Handler<Events[Key]>) { |
因此我们在on
、off
和emit
只能注册或注销事件名和handler的参数与用户定义Events类型一致的事件
代码逻辑
可替换的all
all用来存储用户注册的事件,mitt会初始化一个Map作为默认的,但是用户可以传入自己的Map。
1 | export default function mitt<Events extends Record<EventType, unknown>>( |
on
1 | on<Key extends keyof Events>(type: Key, handler: GenericEventHandler) { |
如果已经注册同名事件则直接将handler推进数组,否则创建个新数组存放hanlder。
off
1 | off<Key extends keyof Events>(type: Key, handler?: GenericEventHandler) { |
有一个秀操作的点,当用户传入handler要删除对应事件名下的对应handler时,有这么一段。
1 | handlers.splice(handlers.indexOf(handler) >>> 0, 1); |
我们知道
indexOf
没有在数组中找到对应的值,那么就会返回-1>>> 0
( 零填充右位移0位)不是没变?- 而
splice(-1, 1)
表示删除最后一位
综合起来看,如果-1 >>> 0
没变那么就会误删最后一个handler。
让我们看看>>> 0
是什么魔法
1 | console.log(-1 >>> 0); // 4294967295 |
js number采用的是IEEE 754双精度浮点,>>>会把number截断成32位int进行位移,更具体的不展开。如果没找到对应的handler,代码即为handlers.splice(4294967295, 1)
,我们当然不可能存如此多的handler,也就不可能删除4294967295下标对应的handler,比起我们判断indexOf
是否为-1缩短了代码(什么叫Microscopic啊?)
emit
- 适配type为*的情况,为所有事件都注册handler,emit时取出*中的所有handler执行
1 | emit<Key extends keyof Events>(type: Key, evt?: Events[Key]) { |
题外话
Once
虽然源码并没有实现,但是既然是事件总线的文章,还是提一提。在我之前面试的时候经常被问到事件总线是如何实现的,其中就有Once的实现,我没有深入思考,只想着实现就完事了:给所有handler用对象包裹一层,然后加一个Once字段标识,在emit时判断Once是否为true,true的话执行完注销事件,但是这样on
和emit
都需要修改,多创建了一个对象并且不优雅。
我们可以充分利用js的灵活性,用一个函数包裹用户的handler,在该函数中off掉handler。
1 | once(type: Key, handler: GenericEventHandler) { |
但是上面的代码有一个很隐蔽的问题:由于是用一个新的函数去包裹,在off时传旧handler去注销once事件无法成功
解决方式
在listener函数中存旧的handler
1 | once(type: Key, handler: GenericEventHandler) { |
不过用这种方式的话,我们的>>>
魔法就不能使用的(这是mitt没有once的原因?)
1 | off<Key extends keyof Events>(type: Key, handler?: GenericEventHandler) { |
unknown
在该Stronger typing #114commit中,大量的any被替换为unknown
关于unknown的特点可看[译] TypeScript 3.0: unknown 类型