深入解析 mitt 和 vueuse useEventBus 的实现

675 阅读8分钟

最近看项目代码的时候发现了 vueuse useEventBus 这个组合函数,就想了解下它是怎么实现的?然后又想到 mitt 是怎么实现的?

首先 mittuseEventBus 都是实现了事件的发布订阅,通过阅读 mitt 、useEventBus 的源码,让我了解了它们是如何实现的以及有什么相同点和不同点!

本着好脑子不如烂笔头的思想有了这篇文章!

使用场景

在 Vue 中,当组件嵌套较深时(如 a > b > c > d > e)这个时候 e 想要直接触发 a 的事件有几种做法:

  1. 通过 props 逐层传递事件:a 一层一层将事件通过 props 传递到 e
  2. 通过 provide 和 inject 注入函数:a 通过 provide 注入函数,e 通过 inject 获取 a 注入的函数进行调用

那反过来呢?

上边的方法只能由上向下传递,无法由下向上传递,即解决了子组件调用父组件函数的问题,无法解决父组件调用子组件函数的问题。

在开发过程中双向事件调用是会存在的,mitt、useEventBus 可以方便简单的解决这类问题!

数据结构 Map、Set

在分析 mitt 和 useEventBus 的实现之前,我们需要了解 js 中的 Map 和 Set 数据结构,因为它们在事件总线的实现中起到了关键作用。

mitt、useEventBus 的事件类型与事件函数的关联是使用 Map 存储的

mitt 的事件函数是使用 Array 进行存储 ,useEventBus 的事件函数则是使用 Set 进行存储

Map

Map 对象保存键值对,并且能够记住键的原始插入顺序。任何值都可以作为 Map 的键或值。

Map 实例方法

  • set 在 Map 对象尾部添加一个键值对。
  • get 获取 Map 对象的键值对。不存在返回 undefined
  • delete 移除 Map 中与这个值相等的元素,返回一个布尔值。
  • clear() 移除 Map 对象内的所有元素。
  • size 返回 Map 长度。
  • has 返回一个布尔值,表示该值在 Map 中存在与否。
  • entrieskeysvalues 均返回一个可迭代对象。

Map 使用示例

let myMap = new Map();

let keyObj = {};
let keyFunc = function() {};
let keyString = 'a string';

// 添加键
myMap.set(keyString, "和键 'a string' 关联的值");
myMap.set(keyObj, "和键keyObj关联的值");
myMap.set(keyFunc, "和键keyFunc关联的值");

myMap.size; // 3

// 读取值
myMap.get(keyString);    // "和键 'a string' 关联的值"
myMap.get(keyObj);       // "和键 keyObj 关联的值"
myMap.get(keyFunc);      // "和键 keyFunc 关联的值"

myMap.get('a string');   // "和键 'a string' 关联的值"
                         // 因为 keyString === 'a string'
myMap.get({});           // undefined, 因为 keyObj !== {}
myMap.get(function() {}); // undefined, 因为 keyFunc !== function () {}

Set

Set 对象允许你存储任何类型(无论是还是对象引用)的唯一值。

这里记住两个知识点:Set 可以存储任何类型的值、Set 中的所有数据都是唯一的

Set 实例方法

  • addSet对象尾部添加一个元素。返回该Set对象。
  • delete 移除Set中与这个值相等的元素,返回一个布尔值。
  • clear() 移除Set对象内的所有元素。
  • size 返回set长度。
  • has 返回一个布尔值,表示该值在Set中存在与否。
  • entrieskeysvalues 均返回一个可迭代对象。

Set 使用示例

let mySet = new Set();

mySet.add(1); // Set [ 1 ]
mySet.add(5); // Set [ 1, 5 ]
mySet.add(5); // Set [ 1, 5 ]
mySet.add("some text"); // Set [ 1, 5, "some text" ]
let o = {a: 1, b: 2};
mySet.add(o);

mySet.add({a: 1, b: 2}); // o 指向的是不同的对象,所以没问题

mySet.has(1); // true
mySet.has(3); // false
mySet.has(5);              // true
mySet.has(Math.sqrt(25));  // true
mySet.has("Some Text".toLowerCase()); // true
mySet.has(o); // true

mySet.size; // 5

mySet.delete(5);  // true,  从set中移除5
mySet.has(5);     // false, 5已经被移除

mySet.size; // 4, 刚刚移除一个值

// 迭代整个set
// 按顺序输出:1, "some text", {"a": 1, "b": 2}, {"a": 1, "b": 2}
for (let item of mySet) console.log(item);

分析 mitt 的实现

mitt 是一个简单的事件发布订阅库,实现了一个简单但功能完整的事件总线

将源码转换为 js 后如下:

image.png

首先使用 Map 创建用于存储事件函数的变量,返回了 all、on、off、emit

all 存储所有事件处理函数

all 是存储所有事件处理函数的 Map 实例

你可以在创建 miit 的时候传入自定义的 Map 对象,将其返回可以让用户手动操作这个对象

image.png

image.png

image.png

on 注册事件监听器

on 是用来给指定的类型绑定事件,两个参数:事件类型、事件处理函数

可以通过设置事件类型为 * 来监听所有事件类型的触发

image.png

  1. 首先获取该事件已经绑定的函数
  2. 如果该事件类型已存在处理函数,则将新处理函数添加到数组中
  3. 如果该事件类型还没有处理函数,则创建新数组存储事件函数

off 移除事件监听器

off 是用来移除指定类型的绑定事件,两个参数:事件类型、事件处理函数

image.png

  1. 首先获取该事件已经绑定的函数
  2. 判断指定的类型的事件是否存在
  3. 判断 “事件处理函数” 是否存在,
  4. 如果存在则使用 splice 移除 indexOf 查找到指定下标的事件函数 5. 其 >>> 0 有些魔法的感觉,可以少写 indexOf 返回 -1 的处理逻辑
    1. 如果 indexOf 返回 -1(未找到),-1 >>> 0 会转换为 4294967295
    2. 确保 splice 的第一个参数为非负整数
    3. 当 handler 不存在时,splice 会尝试从一个很大的索引处删除,实际上不会删除任何元素
  5. 事件处理函数如果不存在,则清空该事件类型的所有处理函数

emit 触发事件

emit 用于触发指定类型的事件函数,两个参数:事件类型、传递给事件函数的参数

image.png

  1. 首先获取该事件已经绑定的函数
  2. 事件函数存在,则通过 slice() 创建副本来遍历,从而避免在触发过程中对原数组的修改导致问题
  3. 获取事件类型是 * 的事件函数
  4. 通配符 * 的事件函数存在,遍历调用所有事件函数,不同于普通事件函数的调用,通配符 * 的事件函数会传递两个参数: 触发的事件类型、事件函数参数

分析 useEventBus 的实现

useEventBus 是一个简单的事件总线的实现类似 vue2 中的 eventBus

将源码转换为 js 后如下:

image.png

这里同样是使用了 Map 来存储事件函数

image.png

useEventBus 接收一个 key 参数用于绑定触发对应的事件函数,然后返回了 on, once, off, emit, reset 函数

getCurrentScope

getCurrentScope 是 vue3 导出的一个方法

image.png

useEventBus 执行时通过 getCurrentScope 获取当前的作用域,用于自动清理监听器

on 注册事件监听器

接收一个事件监听器函数作为参数,这里需要注意下 listener 会接收两个可选参数

type EventBusListener<T = unknown, P = any> = (event: T, payload?: P) => void

function on(listener) {
    // 获取当前 key 对应的监听器集合,如果不存在则创建新的 Set
    const listeners = (events.get(key) || new Set())
    
    // 将接收到的事件监听器函数添加到 Set 集合中
    listeners.add(listener)
    
    // 更新 events 中对应 key 的事件监听器函数
    events.set(key, listeners)

    // 创建取消监听的函数
    const _off = () => off(listener)
    
    // 当作用域销毁时,自动取消监听
    // 将清理函数添加到 Vue 的作用域清理队列中
    scope?.cleanups?.push(_off)
    
    // 返回取消监听的函数
    return _off
}

once 注册只触发一次事件监听器,触发后自动解除监听

接收一个事件监听器函数作为参数

function once(listener) {
    // 创建一个包装函数,在调用原始监听器之前先解除监听,这样就可以让事件监听函数只触发一次
    function _listener(...args) {
      // 执行 off 会在移除这个事件监听函数
      off(_listener)
      
      // 触发事件监听函数
      listener(...args)
    }
    
    // 将包装函数传递给 on 进行注册
    return on(_listener)
}

off 移除指定的事件监听器

接收一个事件监听器函数作为参数

function off(listener) {
    // 获取当前 key 对应的事件监听器集合
    const listeners = events.get(key)
    
    // 如果不存在直接返回
    if (!listeners)
      return

    // listeners 的数据结构是 Set,所以可以使用 delete 方法移除对应的事件监听器
    listeners.delete(listener)

    // listeners 的数据结构是 Set 可以通过 size 方法获取有多少个事件监听器
    // 如果没有剩余的监听器,则完全删除该事件总线
    if (!listeners.size)
      reset()
}

reset 清除事件总线

function reset() {
    // events 的数据结构是 Map,可以直接使用 delete 删除对应的 key
    // 相当于把一个对象的 字段删除了
    events.delete(key)
}

emit 触发事件监听函数

接收两个可选参数:event, payload 会直接传递给事件监听函数

function emit(event, payload) {
    // 遍历所有监听器并调用它们,传入接收到的参数
    events.get(key)?.forEach(v => v(event, payload))
}

使用示例

mittuseEventBus 的使用有点相似又有点不同

  1. mitt 可以在所有的 js 运行时中使用,useEventBus 只能在 vue 中使用
  2. 在 vue 中使用:mitt 在组件卸载时需要手动移除事件监听,useEventBus 会自动处理
  3. mitt 可以直观的定义一组多个事件且有良好的 ts 类型提示,useEventBus 则更倾向于单个事件的处理

mitt

创建一个 emitter 导出

import mitt from 'mitt'

type EventBus = {
  test: {
    text: string
    status: boolean
  }
  test1: {
    status: string
  }
}

export const emitter = mitt<EventBus>()

在需要使用的地方导入然后使用

import { emitter } from './components/mittBus.ts'

// 触发
emitter.emit('test', { text: 'test',status: true })

// 监听
emitter.on('test', (e) => {
  console.log(e)
})

emitter.emit('test1', { status: 'test1' })

emitter.on('test1', (e) => {
  console.log(e)
})

可以按照功能 or 模块创建多个 emitter 进行使用

useEventBus

export const Test = Symbol('test')

const testBus = useEventBus<string, { text: string } >(Test)

testBus.emit('test', { text: '123' })

testBus.on((type, data) => {
  console.log(type, data)
})
export const Test1: EventBusKey<{ status: boolean }> = Symbol('test1')

const test1Bus = useEventBus(Test1)

test1Bus.emit({ status: true })

test1Bus.on((status) => {
  console.log(status)
})

都看到这里你学会了吗?