vue3事件总线:实现mitt源码和TS类型标注

mitt源码深度剖析

大家好,我是晴天同学,这是我的第一篇博客,有什么写的不对的地方,欢迎大家在评论区指正!

介绍

mitt是一个微型(~200b)的订阅与发布模式的库,简单的100多行代码(包括类型与注释)实现了事件的订阅(on)、发布(emit)、取消订阅(off),并支持vue和react,下面是它的API:

  1. all:用于存储所有事件的Map结构
  2. on:用于给指定类型注册处理事件
  • type string|symbol 要侦听的事件类型或'*'所有事件
  • handler **Function**响应给定事件而调用的函数
  1. off:删除给定类型的事件处理程序。如果handler省略,则删除给定类型的所有处理程序。
  • type string|symbol 要取消注册的事件类型handler,或者'*'
  • handler Function 要删除的处理函数
  1. emit:调用给定类型的所有处理程序。如果存在,'*'则在类型匹配的处理程序之后调用处理程序。 注意:不支持手动触发"*"处理程序。
  • type string|symbol 要调用的事件类型
  • evt any? 任何值(推荐使用对象并且功能强大),传递给该type的所有处理程序

下面我们学习一下它的原理。

原理

graph LR A(订阅者A) -- on --> C(调度中心all) <-- emit --- D(发布者A) B(订阅者B) -- on --> C(调度中心all) <-- emit --- E(发布者B)

通过上述图表可知,订阅者通过调用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

graph LR A[on] --> B{all中有无type?} B -->|有| C[将事件handler加入到该type的数组中] B -->|无| D[创建type将handler添加到该type的空数组中]

通过上述流程图可以看到,当我们调用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

graph LR A[off] --> B{all中有无type?} B -->|有| C{off有无handler参数?} B -->|无| D[不做处理] C -->|有| F[将事件handler从该type的数组中移除] C -->|无| G[该type对应的所有事件清除]

通过上述流程图可以看到,当我们调用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

graph LR A[emit] --> B{all中有无type?} B -->|有| C[将type对应的事件数组依次取出执行] B -->|无| D[不做处理] A[emit] --> E{all中有无通配符*?} E -->|有| F[将所有type对应的事件数组依次取出执行] E -->|无| G[不做处理]

通过上述流程图可以看到,当我们使用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()函数增加类型,并可以正确的推导类型结构和参数类型,就必须需要一个泛型EventsEvents是一个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|'*'分成两种情况去看:

  1. 正常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]>
>;
  1. 通配符 * 如果传入的是通配符 *,我们的事件处理函数有两个参数,一个是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!);
        });
      }
    },
  };
}

参考

Mitt 源码动画解析

Vue3通信方式之Mitt源码学习, 从Mitt开始学习源码

相关推荐
new出一个对象1 小时前
uniapp接入BMapGL百度地图
javascript·百度·uni-app
你挚爱的强哥2 小时前
✅✅✅【Vue.js】sd.js基于jQuery Ajax最新原生完整版for凯哥API版本
javascript·vue.js·jquery
前端Hardy3 小时前
纯HTML&CSS实现3D旋转地球
前端·javascript·css·3d·html
susu10830189113 小时前
vue3中父div设置display flex,2个子div重叠
前端·javascript·vue.js
小镇程序员6 小时前
vue2 src_Todolist全局总线事件版本
前端·javascript·vue.js
疯狂的沙粒6 小时前
对 TypeScript 中函数如何更好的理解及使用?与 JavaScript 函数有哪些区别?
前端·javascript·typescript
瑞雨溪6 小时前
AJAX的基本使用
前端·javascript·ajax
力透键背6 小时前
display: none和visibility: hidden的区别
开发语言·前端·javascript
程楠楠&M6 小时前
node.js第三方Express 框架
前端·javascript·node.js·express
weiabc6 小时前
学习electron
javascript·学习·electron