源码地址

  1. mitt:https://github.com/developit/mitt
  2. tiny-emitter:https://github.com/scottcorgan/tiny-emitter

mitt 源码解读

1. package.json 项目 build 打包(运用到包暂不深究,保留个印象即可)

执行 npm run build:

  1. //
  2. "scripts": {
  3. ...
  4. "bundle": "microbundle -f es,cjs,umd",
  5. "build": "npm-run-all --silent clean -p bundle -s docs",
  6. "clean": "rimraf dist",
  7. "docs": "documentation readme src/index.ts --section API -q --parse-extension ts",
  8. ...
  9. },

截屏2021-10-11 下午5.22.17.png

  1. {
  2. "name": "mitt", // package name
  3. ...
  4. ...
  5. "module": "dist/mitt.mjs", // ES Modules output bundle
  6. "main": "dist/mitt.js", // CommonJS output bundle
  7. "jsnext:main": "dist/mitt.mjs", // ES Modules output bundle
  8. "umd:main": "dist/mitt.umd.js", // UMD output bundle
  9. "source": "src/index.ts", // input
  10. "typings": "index.d.ts", // TypeScript typings directory
  11. "exports": {
  12. "import": "./dist/mitt.mjs", // ES Modules output bundle
  13. "require": "./dist/mitt.js", // CommonJS output bundle
  14. "default": "./dist/mitt.mjs" // Modern ES Modules output bundle
  15. },
  16. ...
  17. }

2. 如何调试查看分析?

使用 microbundle watch 命令,新增 script,执行 npm run dev:

  1. "dev": "microbundle watch -f es,cjs,umd"

对应目录新增入口,比如 test.js,执行 node test.js 测试功能:

  1. const mitt = require('./dist/mitt');
  2. const Emitter = mitt();
  3. Emitter.on('test', (e, t) => console.log(e, t));
  4. Emitter.emit('test', { a: 12321 });

对应源码 src/index.js 也依然可以加相关的 log 进行查看,代码变动后会触发重新打包

3. TS 声明

使用上可以(官方给的例子),比如定义 foo 事件,回调函数里面的参数要求是 string 类型,可以想象一下源码 TS 是怎么定义的:

  1. import mitt from 'mitt';
  2. // key 为事件名,key 对应属性为回调函数的参数类型
  3. type Events = {
  4. foo: string;
  5. bar?: number; // 对应事件允许不传参数
  6. };
  7. const emitter = mitt<Events>(); // inferred as Emitter<Events>
  8. emitter.on('foo', (e) => {}); // 'e' has inferred type 'string'
  9. emitter.emit('foo', 42); // Error: Argument of type 'number' is not assignable to parameter of type 'string'. (2345)
  10. emitter.on('*', (type, e) => console.log(type, e) )

源码内关于 TS 定义(关键几句):

  1. export type EventType = string | symbol;
  2. // Handler 为事件(除了*事件)回调函数定义
  3. export type Handler<T = unknown> = (event: T) => void;
  4. // WildcardHandler 为事件 * 回调函数定义
  5. export type WildcardHandler<T = Record<string, unknown>> = (
  6. type: keyof T, // keyof T,事件名
  7. event: T[keyof T] // T[keyof T], 事件名对应的回调函数入参类型
  8. ) => void;
  9. export interface Emitter<Events extends Record<EventType, unknown>> {
  10. // ...
  11. on<Key extends keyof Events>(type: Key, handler: Handler<Events[Key]>): void;
  12. on(type: '*', handler: WildcardHandler<Events>): void;
  13. // ...
  14. emit<Key extends keyof Events>(type: Key, event: Events[Key]): void;
  15. // 这句主要兼容无参数类型的事件,如果说事件对应回调必须传参,使用中如果未传,那么会命中 never,如下图
  16. emit<Key extends keyof Events>(type: undefined extends Events[Key] ? Key : never): void;
  17. }

以下是会报 TS 错误:
截屏2021-10-25 下午2.28.37.png
以下是正确的:
截屏2021-10-25 下午2.29.17.png

4. 主逻辑

  1. 整体就是一个 function,输入为事件 Map,输出为 all 所有事件 Map,还有 on,emit,off 几个关于事件方法:

    1. export default function mitt<Events extends Record<EventType, unknown>>(
    2. // 支持 all 初始化
    3. all?: EventHandlerMap<Events>
    4. ): Emitter<Events> {
    5. // 内部维护了一个 Map(all),Key 为事件名,Value 为 Handler 回调函数数组
    6. all = all || new Map();
    7. return {
    8. all, // 所有事件 & 事件对应方法
    9. emit, // 触发事件
    10. on, // 订阅事件
    11. off // 注销事件
    12. }
    13. }
  2. on 为【事件订阅】,push 对应 Handler 到对应事件 Map 的 Handler 回调函数数组内(可熟悉下 Map 相关API https://developer.mozilla.org/zh-CN/docs/Web/JavaScript/Reference/Global_Objects/Map):

    1. on<Key extends keyof Events>(type: Key, handler: GenericEventHandler) {
    2. // Map get 获取
    3. const handlers: Array<GenericEventHandler> | undefined = all!.get(type);
    4. // 如果已经初始化过的话,是个数组,直接 push 即可
    5. if (handlers) {
    6. handlers.push(handler);
    7. }
    8. // 如果第一次注册事件,则 set 新的数组
    9. else {
    10. all!.set(type, [handler] as EventHandlerList<Events[keyof Events]>);
    11. }
    12. }
  3. off 为【事件注销】,从对应事件 Map 的 Handlers 中,splice 掉:

    1. off<Key extends keyof Events>(type: Key, handler?: GenericEventHandler) {
    2. // Map get 获取
    3. const handlers: Array<GenericEventHandler> | undefined = all!.get(type);
    4. // 如果有事件列表,则进入,没有则忽略
    5. if (handlers) {
    6. // 对 handler 事件进行 splice 移出数组
    7. // 这里是对找到的第一个 handler 进行移出,所以如果订阅了多次,只会去除第一个
    8. // handlers.indexOf(handler) >>> 0,>>> 为无符号位移
    9. // 关于网上对 >>> 用法说明: It doesn't just convert non-Numbers to Number, it converts them to Numbers that can be expressed as 32-bit unsigned ints.
    10. if (handler) {
    11. handlers.splice(handlers.indexOf(handler) >>> 0, 1);
    12. }
    13. // 如果不传对应的 Handler,则为清空事件对应的所有订阅
    14. else {
    15. all!.set(type, []);
    16. }
    17. }
    18. }
  4. emit 为【事件触发】,读取事件 Map 的 Handlers,循环逐一触发,如果订阅了 全事件,则读取 的 Handlers 逐一触发:

    1. emit<Key extends keyof Events>(type: Key, evt?: Events[Key]) {
    2. // 获取对应 type 的 Handlers
    3. let handlers = all!.get(type);
    4. if (handlers) {
    5. (handlers as EventHandlerList<Events[keyof Events]>)
    6. .slice()
    7. .map((handler) => {
    8. handler(evt!);
    9. });
    10. }
    11. // 获取 * 对应的 Handlers
    12. handlers = all!.get('*');
    13. if (handlers) {
    14. (handlers as WildCardEventHandlerList<Events>)
    15. .slice()
    16. .map((handler) => {
    17. handler(type, evt!);
    18. });
    19. }
    20. }

    为什么是使用 slice().map() ,而不是直接使用 forEach() 进行触发?具体可查看:https://github.com/developit/mitt/pull/109,具体可以拷贝相关代码进行调试,直接更换成 forEach 的话,针对以下例子所触发的 emit 是错误的: ```javascript import mitt from ‘./mitt’

type Events = { test: number }

const Emitter = mitt() Emitter.on(‘test’, function A(num) { console.log(‘A’, num) Emitter.off(‘test’, A) }) Emitter.on(‘test’, function B() { console.log(‘B’) }) Emitter.on(‘test’, function C() { console.log(‘C’) })

Emitter.emit(‘test’, 32432) // 触发 A,C 事件,B 会被漏掉 Emitter.emit(‘test’, 32432) // 触发 B,C,这个是正确的

// 原因解释: // forEach 时,在 Handlers 循环过程中,同时触发了 off 操作 // 按这个例子的话,A 是第一个被注册的,所以第一个会被 slice 掉 // 因为 array 是引用类型,slice 之后,那么 B 函数就会变成第一个 // 但此时遍历已经到第二个了,所以 B 函数就会被漏掉执行

// 解决方案: // 所以对数组进行 [].slice() 做一个浅拷贝,off 的 Handlers 与 当前循环中的 Handlers 处理成不同一个 // [].slice.forEach() 效果其实也是一样的,用 map 的话个人感觉不是很语义化

  1. <a name="f2FyC"></a>
  2. ## 5. 小结
  3. - TS keyof 的灵活运用
  4. - **undefined extends Events[Key]** ? Key : never,为 TS 的条件类型([https://www.typescriptlang.org/docs/handbook/2/conditional-types.html](https://www.typescriptlang.org/docs/handbook/2/conditional-types.html))
  5. - undefined extends Events[Key] ? Key : **never**,当我们想要编译器不捕获当前值或者类型时,我们可以返回 never类型。never 表示永远不存在的值的类型
  6. ```javascript
  7. // 来自 typescript 中的 lib.es5.d.ts 定义
  8. /**
  9. * Exclude null and undefined from T
  10. */
  11. type NonNullable<T> = T extends null | undefined ? never : T;
  12. // 如果 T 的值包含 null 或者 undefined,则会 never 表示不允许走到此逻辑,否则返回 T 本身的类型
  • mitt 的事件回调函数参数,只会有一个,而不是多个,如何兼容多个参数的情况,官方推荐是使用 object 的(object is recommended and powerful),这种设计扩展性更高,更值得推荐。截屏2021-10-25 下午4.30.42.png

tiny-emitter 源码解读

1. 主逻辑

  1. 所有方法都是挂载在 E 的 prototype 内的,总共暴露了 once,emit,off,on 四个事件的方法: ```javascript function E () { // Keep this empty so it’s easier to inherit from // (via https://github.com/lipsmack from https://github.com/scottcorgan/tiny-emitter/issues/3) }

// 所有事件都挂载在 this.e 上,是个 object E.prototype = { on: function (name, callback, ctx) {}, once: function (name, callback, ctx) {}, emit: function (name) {}, off: function (name, callback) {} }

module.exports = E; module.exports.TinyEmitter = E;

  1. 2. once 订阅一次事件,当被触发一次后,就会被销毁:
  2. ```javascript
  3. once: function (name, callback, ctx) {
  4. var self = this;
  5. // 构造另一个回调函数,调用完之后,销毁该 callback
  6. function listener () {
  7. self.off(name, listener); // 销毁
  8. callback.apply(ctx, arguments); // 执行
  9. };
  10. listener._ = callback
  11. // on 函数返回 this,所以可以链式调用
  12. return this.on(name, listener, ctx); // 订阅这个构造的回调函数
  13. }
  1. on 事件订阅

    1. on: function (name, callback, ctx) {
    2. var e = this.e || (this.e = {});
    3. // 单纯 push 进去,这里也没有做去重,所以同一个回调函数可以被订阅多次
    4. (e[name] || (e[name] = [])).push({
    5. fn: callback,
    6. ctx: ctx
    7. });
    8. // 返回 this,可以链式调用
    9. return this;
    10. }
  2. off 事件销毁

    1. off: function (name, callback) {
    2. var e = this.e || (this.e = {});
    3. var evts = e[name];
    4. var liveEvents = []; // 保存还有效的 hanlder
    5. // 传递的 callback,如果命中,就不会被放到 liveEvents 里面
    6. // 所以这里的销毁是一次性销毁全部相同的 callback,与 mitt 不一样
    7. if (evts && callback) {
    8. for (var i = 0, len = evts.length; i < len; i++) {
    9. if (evts[i].fn !== callback && evts[i].fn._ !== callback)
    10. liveEvents.push(evts[i]);
    11. }
    12. }
    13. // Remove event from queue to prevent memory leak
    14. // Suggested by https://github.com/lazd
    15. // Ref: https://github.com/scottcorgan/tiny-emitter/commit/c6ebfaa9bc973b33d110a84a307742b7cf94c953#commitcomment-5024910
    16. // 如果没有任何 handler,对应的事件 name 也可以被 delete
    17. (liveEvents.length)
    18. ? e[name] = liveEvents
    19. : delete e[name];
    20. // 返回 this,可以链式调用
    21. return this;
    22. }
  3. emit 事件触发

    1. emit: function (name) {
    2. // 取除了第一位的剩余所有参数
    3. var data = [].slice.call(arguments, 1);
    4. // slice() 浅拷贝
    5. var evtArr = ((this.e || (this.e = {}))[name] || []).slice();
    6. var i = 0;
    7. var len = evtArr.length;
    8. // 循环逐个触发 handler,把 data 传入其中
    9. for (i; i < len; i++) {
    10. evtArr[i].fn.apply(evtArr[i].ctx, data);
    11. }
    12. // 返回 this,可以链式调用
    13. return this;
    14. }

2. 小结

  • return this,支持链式调用
  • emit 事件触发时,[].slice.call(arguments, 1) 剔除第一个参数,获取到剩余的参数列表,再使用 apply 来调用
  • on 事件订阅时,记录的是 { fn, ctx },fn 为回调函数,ctx 支持绑定上下文

mitt 与 tiny-emitter 对比

  • TS 静态类型校验上 mitt > tiny-emitter,开发更友好,对于回调函数参数的管理,tiny-emitter 支持多参数调用的,但是 mitt 提倡使用 object 管理,设计上感觉 mitt 更加友好以及规范
  • 在 off 事件销毁中,tiny-emitter 与 mitt 处理方式不同,tiny-emitter 会一次性销毁所有相同的 callback,而 mitt 则只是销毁第一个
  • mitt 不支持 once 方法,tiny-emitter 支持 once 方法
  • mitt 支持 * 全事件订阅,tiny-emitter 则不支持

Vue eventBus 事件总线(3.x 已废除,2.x 依然存在)

  1. 初始化过程 ```javascript // index.js 调用 initMixin 方法,初始化 _events object initMixin(Vue)

// event.js 定义 initEvents 方法 // vm._events 保存所有事件 & 事件回调函数,是个 object export function initEvents (vm: Component) { vm._events = Object.create(null) // … }

// index.js 调用 eventsMixin,往 Vue.prototype 挂载相关事件方法 eventsMixin(Vue)

// event.js 定义了 eventsMixin 方法 export function eventsMixin (Vue: Class) { // 事件订阅 Vue.prototype.$on = function (event: string | Array, fn: Function): Component {} // 事件订阅执行一次 Vue.prototype.$once = function (event: string, fn: Function): Component {} // 事件退订 Vue.prototype.$off = function (event?: string | Array, fn?: Function): Component {} // 事件触发 Vue.prototype.$emit = function (event: string): Component {} }

  1. 2. $on 事件订阅
  2. ```javascript
  3. // event 是个 string,也可以是个 string 数组
  4. // 说明可以一次性对多个事件,订阅同一个回调函数
  5. Vue.prototype.$on = function (event: string | Array<string>, fn: Function): Component {
  6. const vm: Component = this
  7. if (Array.isArray(event)) {
  8. for (let i = 0, l = event.length; i < l; i++) {
  9. vm.$on(event[i], fn)
  10. }
  11. } else {
  12. // 本质是就是对应 event,push 对应的 fn
  13. (vm._events[event] || (vm._events[event] = [])).push(fn)
  14. // 以下先不展开,关于 hookEvent 的调用说明
  15. // optimize hook:event cost by using a boolean flag marked at registration
  16. // instead of a hash lookup
  17. if (hookRE.test(event)) {
  18. vm._hasHookEvent = true
  19. }
  20. }
  21. return vm
  22. }
  1. $once 事件订阅&执行一次

    1. // 包装一层 on,内包含退订操作以及调用操作
    2. // 订阅的是包装后的 on 回调函数
    3. Vue.prototype.$once = function (event: string, fn: Function): Component {
    4. const vm: Component = this
    5. function on () {
    6. vm.$off(event, on)
    7. fn.apply(vm, arguments)
    8. }
    9. on.fn = fn
    10. vm.$on(event, on)
    11. return vm
    12. }
  2. $off 事件退订

    1. Vue.prototype.$off = function (event?: string | Array<string>, fn?: Function): Component {
    2. const vm: Component = this
    3. // 没有传参数,说明全部事件退订,直接清空
    4. if (!arguments.length) {
    5. vm._events = Object.create(null)
    6. return vm
    7. }
    8. // 存在 event 数组,遍历逐一调用自己
    9. if (Array.isArray(event)) {
    10. for (let i = 0, l = event.length; i < l; i++) {
    11. vm.$off(event[i], fn)
    12. }
    13. return vm
    14. }
    15. // 以下情况为非数组事件名,为单一事件,则获取该事件对应订阅的 callbacks
    16. const cbs = vm._events[event]
    17. // 若 callbacks 为空,什么都不用做
    18. if (!cbs) {
    19. return vm
    20. }
    21. // 如果传入的 fn 为空,说明退订这个事件的所有 callbacks
    22. if (!fn) {
    23. vm._events[event] = null
    24. return vm
    25. }
    26. // callbacks 不为空,并且 fn 不为空,则为退订某个 callback
    27. let cb
    28. let i = cbs.length
    29. while (i--) {
    30. cb = cbs[i]
    31. // 订阅多次的 callback,都会被退订,一次退订所有相同的 callback
    32. if (cb === fn || cb.fn === fn) {
    33. cbs.splice(i, 1)
    34. break
    35. }
    36. }
    37. return vm
    38. }
  3. $emit 事件触发

    1. Vue.prototype.$emit = function (event: string): Component {
    2. const vm: Component = this
    3. if (process.env.NODE_ENV !== 'production') {
    4. const lowerCaseEvent = event.toLowerCase()
    5. if (lowerCaseEvent !== event && vm._events[lowerCaseEvent]) {
    6. tip(
    7. `Event "${lowerCaseEvent}" is emitted in component ` +
    8. `${formatComponentName(vm)} but the handler is registered for "${event}". ` +
    9. `Note that HTML attributes are case-insensitive and you cannot use ` +
    10. `v-on to listen to camelCase events when using in-DOM templates. ` +
    11. `You should probably use "${hyphenate(event)}" instead of "${event}".`
    12. )
    13. }
    14. }
    15. // 获取这个 event 的 callbacks 出来
    16. let cbs = vm._events[event]
    17. if (cbs) {
    18. cbs = cbs.length > 1 ? toArray(cbs) : cbs
    19. // 获取除了第一位,剩余的其他所有参数
    20. const args = toArray(arguments, 1)
    21. const info = `event handler for "${event}"`
    22. // 遍历逐一触发
    23. for (let i = 0, l = cbs.length; i < l; i++) {
    24. // 以下暂不展开,这是 Vue 中对于方法调用错误异常的处理方案
    25. invokeWithErrorHandling(cbs[i], vm, args, vm, info)
    26. }
    27. }
    28. return vm
    29. }

    实现逻辑大致和 mitt,tiny-emitter 一致,也是 pubsub,整体思路都是维护一个 object 或者 Map,on 则是放到数组内,emit 则是循环遍历逐一触发,off 则是查找到对应的 handler 移除数组
    TODO:

  • Vue 中对于方法调用错误异常的处理方案:invokeWithErrorHandling
  • hookEvent 的使用&原理

附录