手写实现事件总线

事件总线是发布-订阅模式的实现,本质上来说就是对同一个对象的引入操作,保证全局通信中介的唯一性,核心目的是解耦组件,由事件总线内部维护事件与函数的映射关系,从而达到执行一致性。

基本实现

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
class EventBus {
constructor() {
this.eventMap = {}
}

on(eventName, eventFn) {
let eventFns = this.eventMap[eventName]
if (!eventFns) {
eventFns = []
this.eventMap[eventName] = eventFns
}
eventFns.push(eventFn)
}

emit(eventName, ...args) {
let eventFns = this.eventMap[eventName]
if (!eventFns) return
for (let eventFn of eventFns) {
eventFn(...args)
}
}

off(eventName, eventFn) {
let eventFns = this.eventMap[eventName]
if (!eventFns) return
for (let i = 0; i < eventFns.length; i++) {
if (eventFns[i] === eventFn) {
eventFns.splice(i, 1)
break
}
}
}

clear() {
this.eventMap = {}
}

}

进阶:实现批量订阅和通配符情况

思路:

  • 创建三个对象

    • 一个对象负责精确事件与函数的联系,因为一个事件可能关联多个函数,所以采用Map结构,将事件名作为键,为了方便对关联的函数进行操作,所以用Set结构存储事件函数作为值。
    • 一个负责存储通配符情况下对应的规则与处理函数,方便结构操作,采用Set存储对象。
    • 一个负责进行对批量订阅的处理,为了解决多个订阅与单个订阅绑定相同函数的冲突,用Map数据结构处理,且此容器仅仅是为了中间存储关系,实际存储还是在精确事件中,为了方便内存管理,采用WeakMap结构。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
class EventBus {
constructor() {
this.listeners = new Map() // 精确事件
this.wildcards = new Set() // 通配符事件
this.batchHandlers = new WeakMap() // 批量订阅的映射
}

// 订阅事件(支持字符串、数组、通配符)
on(events, handler) {
// 判断传入的是否是事件是否数组,如果只是一个字符串,则将其转为数组
if (typeof events === 'string') events = [events]
// 遍历事件数组
events.forEach(event => this._registerEvent(event, handler))
}

ons(events, handler) {
const wrapper = (...args) => handler(...args)
this.batchHandlers.set(handler, wrapper)
events.forEach(event => this._registerEvent(event, wrapper))
}

// 触发事件
emit(event, ...args) {
// 精确匹配
if (this.listeners.has(event)) {
this.listeners.get(event).forEach(handler => handler(...args))
}

// 通配符匹配
this.wildcards.forEach(({ pattern, handler }) => {
if (pattern.test(event)) handler(...args)
})
}


off(event, handler) {
const handlers = this.listeners.get(event)
if (handlers) handlers.delete(handler)
}

offs(events, handler) {
const wrapper = this.batchHandlers.get(handler)
if (!wrapper) return

events.forEach(event => {
const handlers = this.listeners.get(event)
if (handlers) handlers.delete(wrapper)
})
this.batchHandlers.delete(handler)
}

_registerEvent(event, handler) {
// 判断是否为通配符匹配
if (event.includes('*')) {
this.wildcards.add({
// 制定通配符匹配规则
// 如 event = 'test.*' 将其转换为正则模式 => /^test..*$/
// 但这样会存在问题,如果是test1,也符合/^test..*$/,所以我们可以对其进行优化变为 => /^test\..*$/
// pattern: new RegExp(`^${event.replace(/\*/g, '.*')}$`)
pattern: event.includes('.') ? new RegExp(`^${event.replace(/\.\*/g, '\\..*')}$`) : new RegExp(`^${event.replace(/\*/g, '.*')}$`),
handler
})
} else {
// 如果不包含通配符,将其以Set形式加入到精确事件集合中
if (!this.listeners.has(event)) this.listeners.set(event, new Set())
this.listeners.get(event).add(handler)
}
}
}