本文参加了由公众号@若川视野 发起的每周源码共读活动,点击了解详情一起参与。
1. 前言
这次学习的项目是
- mitt
-
1.1 你能学到😎
发布订阅模式
- 了解 mitt、tiny-emitter 用法
- 以及他们的应用场景
- 具体实现
2. 这个库,是干啥的😶
老规矩,先看READMEmitt
Tiny 200b functional event emitter / pubsub.
- Microscopic: weighs less than 200 bytes gzipped
- Useful: a wildcard “*” event type listens to all events
- Familiar: same names & ideas as Node’s EventEmitter
- Functional: methods don’t rely on this
- Great Name: somehow mitt wasn’t taken
Mitt was made for the browser, but works in any JavaScript runtime. It has no dependencies and supports IE9+.
微型 200b 功能事件发射器 / pubsub。- 微观: gzip 压缩后的重量小于 200 字节
- 有用:通配符”*”事件类型监听所有事件
- 熟悉:与Node 的 EventEmitter相同的名称和想法
- 函数式:方法不依赖this
- 好名字:不知怎的,手套没有被拿走(这里应该是说该名字没有被别人用过
Mitt 是为浏览器设计的,但适用于任何 JavaScript 运行时。它没有依赖项,支持 IE9+。
tiny-emitter
A tiny (less than 1k) event emitter library.
一个很小(小于 1k)的事件发射器库。
从README中即可得知这两个库是用于发布订阅(上面谷歌翻译的发射,应该是发布)以及监听
2. 发布订阅模式🤔
这个所谓的发布订阅以及监听,就有点像是我们常用的onClick事件等监听~,但不局限于此~
我们为什么需要这个模式

看起来非常的直观、简单,那就让我们来看看是怎么实现的
3 看看 源码👻
const emitter = mitt()
// listen to an event emitter.on(‘foo’, e => console.log(‘foo’, e) )
// listen to all events emitter.on(‘*’, (type, e) => console.log(type, e) )
// fire an event emitter.emit(‘foo’, { a: ‘b’ })
// clearing all events emitter.all.clear()
// working with handler references: function onFoo() {} emitter.on(‘foo’, onFoo) // listen emitter.off(‘foo’, onFoo) // unlisten
很明显,就是导出了一个`mitt`函数,调用后返回一个`emitter`对象,对象上有这几个属性/方法:- `on(type, handler)`:进行事件的订阅- `off(type, [handler])`:取消订阅给定类型的`handler`- `emit`:调用给定类型的所有`handler`- `all`:存储事件类型和事件处理函数的映射Map<a name="vafax"></a>### 源码源码在 [mitt/src/index.ts](https://github.com/developit/mitt/blob/main/src/index.ts),是ts的,希望大家都会😏如果不会,可以先编译一下为js,不过其实看ts代码时都没什么难的,就是类型而已<br />这里就是一些ts的接口,定义参数类型什么的,不是重点```typescriptexport type EventType = string | symbol;// An event handler can take an optional event argument// and should not return a valueexport type Handler<T = unknown> = (event: T) => void;export type WildcardHandler<T = Record<string, unknown>> = (type: keyof T,event: T[keyof T]) => void;// An array of all currently registered event handlers for a typeexport type EventHandlerList<T = unknown> = Array<Handler<T>>;export type WildCardEventHandlerList<T = Record<string, unknown>> = Array<WildcardHandler<T>>;// A map of event types and their corresponding event handlers.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方法,(为了减少篇幅,我删除了一些原文中的英文注释),详细中文注释如下:
//导出mitt函数,调用后返回一个Emitter对象export default function mitt<Events extends Record<EventType, unknown>>(all?: EventHandlerMap<Events>//?: 表示all是可选参数): Emitter<Events> {type GenericEventHandler =| Handler<Events[keyof Events]>| WildcardHandler<Events>;all = all || new Map(); //支持传入all参数,如果不传那就new Map一个赋值给allreturn {//存储事件类型和事件处理函数的映射Mapall,//on函数注册事件,type为类型,handler为处理函数,存储在handlers数组中//用数组是因为可能监听一个事件,有多个处理函数on<Key extends keyof Events>(type: Key, handler: GenericEventHandler) {const handlers: Array<GenericEventHandler> | undefined = all!.get(type);if (handlers) {//有值直接放入handlers.push(handler);}else {//无值就初始化//type为属性值,handlers数组为属性值//!.是TS断言,意思是all中必有set这个东东all!.set(type, [handler] as EventHandlerList<Events[keyof Events]>);}},//取消某个事件的回调函数off<Key extends keyof Events>(type: Key, handler?: GenericEventHandler) {//根据属性名type取对应的属性值handlers处理函数数组const handlers: Array<GenericEventHandler> | undefined = all!.get(type);if (handlers) {if (handler) {//如果有传入hanlder//就找到对应的位置,并将其从数组中删除,这个>>>是个亮点后文详细写一下handlers.splice(handlers.indexOf(handler) >>> 0, 1);}else {//没有传入handler就删除全部all!.set(type, []);}}},//根据type找到对应的事件处理函数数组并全部执行emit<Key extends keyof Events>(type: Key, evt?: Events[Key]) {//根据属性名type找到属性值handlerslet handlers = all!.get(type);if (handlers) {(handlers as EventHandlerList<Events[keyof Events]>).slice()//生成新数组,使得后面的map不会改变原数组.map((handler) => {//依次执行处理函数,并传入对应的参数handler(evt!);});}//获取监听全部事件的处理函数,type事件∈全部事件~handlers = all!.get('*');if (handlers) {//同上(handlers as WildCardEventHandlerList<Events>).slice().map((handler) => {handler(type, evt!);});}}};}
Map
Map 是一个带键的数据项的集合,就像一个 Object 一样。 但是它们最大的差别是 Map 允许任何类型的键(key),如果你还不了解其API,可以点击链接查看。
all简洁地建立映射
if (handlers) {if (handler) {//如果有传入hanlder//就找到对应的位置,并将其从数组中删除,这个>>>是个亮点后文详细写一下handlers.splice(handlers.indexOf(handler) >>> 0, 1);}else {//没有传入handler就删除全部all!.set(type, []);}}
利用Map存储对象的方式是引用,简洁地完成了有值取值无值初始化的效果
无符号右移(>>>)
>>>运算符执行无符号右移位运算。- 它把无符号的 32 位整数所有数位整体右移。
- 对于无符号数或正数右移运算,无符号右移与有符号右移运算的结果是相同的
- 对于负数来说,无符号右移将使用 0 来填充所有的空位
indexOf如果找不到对应的下标,是会返回一个 -1 的,而 -1 在slice中是有效的,即倒数第一个,很明显将不符合要求
而-1>>>0后会返回一个很大的数 4294967295,不会修改到原数组,而正数>>>0后不会改变
3.2 tiny-emitter
API
var Emitter = require('tiny-emitter');var emitter = new Emitter();emitter.on('some-event', function (arg1, arg2, arg3) {//});emitter.emit('some-event', 'arg1 value', 'arg2 value', 'arg3 value');
on(name, callback, ctx)once(name, callback, ctx):只订阅一次事件emit(name)off(name, callback)
多了一个once,从接收参数来看,还多了一个ctx即执行上下文~
源码
直接看源码~ 在 tiny-emitter/index.js
(为了减少篇幅,我删除了一些原文中的英文注释),详细中文注释如下:
//定义函数Efunction E () {//使其为空让其很容易继承}//在原型上修改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) {var self = this;//防止里面的函数this丢失function listener () {self.off(name, listener);//注销订阅callback.apply(ctx, arguments);//执行处理函数};listener._ = callback//存储在_中,方便后面off时查找//将封装之后的事件处理函数作为on参数之一 返回一个onreturn this.on(name, listener, ctx);},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循环遍历执行for (i; i < len; i++) {evtArr[i].fn.apply(evtArr[i].ctx, data);}return this;},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++) {//fn._ 就是once处理过的if (evts[i].fn !== callback && evts[i].fn._ !== callback)liveEvents.push(evts[i]);}}//处理函数数组为空的话就要删除,不为空的话就更新到e数组中(liveEvents.length)? e[name] = liveEvents: delete e[name];return this;}};module.exports = E;module.exports.TinyEmitter = E;
支持链式调用
4. 学习资源
-
4. 总结 & 收获👩🍳
两库之同
都非常小,但功能也算齐全,缺陷的话就是用js的时候像传参什么的没有报错,当然用ts的话就能补足这个缺陷了
- 这两个库实现发布订阅模式的方式大同小异~
- 使用一个结构存储事件类型与处理函数的映射,通过该映射操作类型对应的处理函数:
- 调用
- 取消订阅
- 使用一个结构存储事件类型与处理函数的映射,通过该映射操作类型对应的处理函数:
API 基本也相同
一个是用 Map 一个使用对象
{}- ps:好像
mitt以前版本也是用对象的
- ps:好像
- emit
- 支持
'*'订阅全部事件 - all 获取全部事件
- 返回一个用函数生成的对象,属性都是来自自身
- 支持
tiny-emitter
发现 developit是个宝藏开源项目开发者啊~ 后续可以多读读他的库嘿嘿,看起来都很小而美,并且不少与
react相关的🌊如果有所帮助,欢迎点赞关注,一起进步⛵
