最近看项目代码的时候发现了 vueuse useEventBus 这个组合函数,就想了解下它是怎么实现的?然后又想到 mitt 是怎么实现的?
首先 mitt 、useEventBus 都是实现了事件的发布订阅,通过阅读 mitt 、useEventBus 的源码,让我了解了它们是如何实现的以及有什么相同点和不同点!
本着好脑子不如烂笔头的思想有了这篇文章!
使用场景
在 Vue 中,当组件嵌套较深时(如 a > b > c > d > e)这个时候 e 想要直接触发 a 的事件有几种做法:
- 通过
props逐层传递事件:a 一层一层将事件通过props传递到 e - 通过
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 对象的键值对。不存在返回undefineddelete移除 Map 中与这个值相等的元素,返回一个布尔值。clear()移除 Map 对象内的所有元素。size返回 Map 长度。has返回一个布尔值,表示该值在 Map 中存在与否。entries、keys、values均返回一个可迭代对象。
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 实例方法
add在Set对象尾部添加一个元素。返回该Set对象。delete移除Set中与这个值相等的元素,返回一个布尔值。clear()移除Set对象内的所有元素。size返回set长度。has返回一个布尔值,表示该值在Set中存在与否。entries、keys、values均返回一个可迭代对象。
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 后如下:
首先使用 Map 创建用于存储事件函数的变量,返回了 all、on、off、emit
all 存储所有事件处理函数
all 是存储所有事件处理函数的 Map 实例
你可以在创建 miit 的时候传入自定义的 Map 对象,将其返回可以让用户手动操作这个对象
on 注册事件监听器
on 是用来给指定的类型绑定事件,两个参数:事件类型、事件处理函数
可以通过设置事件类型为 * 来监听所有事件类型的触发
- 首先获取该事件已经绑定的函数
- 如果该事件类型已存在处理函数,则将新处理函数添加到数组中
- 如果该事件类型还没有处理函数,则创建新数组存储事件函数
off 移除事件监听器
off 是用来移除指定类型的绑定事件,两个参数:事件类型、事件处理函数
- 首先获取该事件已经绑定的函数
- 判断指定的类型的事件是否存在
- 判断 “事件处理函数” 是否存在,
- 如果存在则使用
splice移除indexOf查找到指定下标的事件函数 5. 其>>> 0有些魔法的感觉,可以少写indexOf 返回 -1的处理逻辑- 如果 indexOf 返回 -1(未找到),-1 >>> 0 会转换为 4294967295
- 确保 splice 的第一个参数为非负整数
- 当 handler 不存在时,splice 会尝试从一个很大的索引处删除,实际上不会删除任何元素
- 事件处理函数如果不存在,则清空该事件类型的所有处理函数
emit 触发事件
emit 用于触发指定类型的事件函数,两个参数:事件类型、传递给事件函数的参数
- 首先获取该事件已经绑定的函数
- 事件函数存在,则通过 slice() 创建副本来遍历,从而避免在触发过程中对原数组的修改导致问题
- 获取事件类型是
*的事件函数 - 通配符
*的事件函数存在,遍历调用所有事件函数,不同于普通事件函数的调用,通配符*的事件函数会传递两个参数: 触发的事件类型、事件函数参数
分析 useEventBus 的实现
useEventBus 是一个简单的事件总线的实现类似 vue2 中的 eventBus
将源码转换为 js 后如下:
这里同样是使用了 Map 来存储事件函数
useEventBus 接收一个 key 参数用于绑定触发对应的事件函数,然后返回了 on, once, off, emit, reset 函数
getCurrentScope
getCurrentScope 是 vue3 导出的一个方法
在 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))
}
使用示例
mitt 与 useEventBus 的使用有点相似又有点不同
- mitt 可以在所有的 js 运行时中使用,useEventBus 只能在 vue 中使用
- 在 vue 中使用:mitt 在组件卸载时需要手动移除事件监听,useEventBus 会自动处理
- 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)
})
都看到这里你学会了吗?