事件总线机制

本博客 hjy-xh,转载请申明出处

介绍

事件总线(EventBus) 是一种消息传递的方式

它可以让两个没有关联的组件进行通信,起到数据传输的作用

举个例子,当module N发布了Event 1消息,订阅该消息的module 1就会收到相关消息(过程如下所示)

场景举例

现在有一所学校,学校里面有一些事件发生,学生能够根据事件类型做出相应的动作

用代码表述

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
// EventBus就是事件总线,这里可以思考一下它应该具备什么功能以及如何编写
let EB = new EventBus();

// 学生对事件类型做出的反应
EB.$on('上课', (name) => {
console.info(`学生${name}到上课地点`);
})
EB.$on("教室上课", (name, course) => {
console.info(`学生${name}去教室上${course}课`);
})
let id = EB.$on("户外上课", (name, course) => {
console.info(`学生${name}去户外上${course}课`);
})
EB.$once("献血", (name) => {
console.info(`学生${name}去献血`);
})

// 学校中事件发生
EB.$emit('上课', '张三'); // 学生张三到上课地点
EB.$emit('上课', '李四'); // 学生李四到上课地点
EB.$emit('教室上课', '张三', '编程'); // 学生张三去教室上编程课
EB.$emit('户外上课', '张三', '田径'); // 学生张三去户外上田径课
EB.$off('户外上课', id);
EB.$emit('户外上课', '李四', '足球');
EB.$emit('献血', '王五'); // 学生王五去献血
EB.$emit('献血', '马六');
// console.log(EB)
EB.$clear()
// console.log(EB)

从上面的代码可以大概了解到,事件总线应该具备五种方法,对应着下一部分的API设计

API设计

  • 发布消息(emit)
    • 能够表示消息类型,可能还携带参数
  • 订阅消息(on)
    • 能够知道具体的消息类型,并执行回调,回调的参数就是发布消息时携带的参数
  • 取消订阅(off)
    • 能够知道具体的消息类型,取消订阅该消息类型,即回调函数不再执行
  • 仅订阅一次消息(once)
    • 能够知道具体的消息类型,执行一次回调后,再次接收相同消息,不再执行回调
  • 清除某个或所有事件(clear)
    • 如果指定了某个消息类型,则清除该消息类型的回调,否则全部清除

代码实现

基础实现(仅包含订阅和发布)

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.eventObj = {}; // interface EveneObj { [name: string]: Function[] }
}

$on(name, callback) {
if (!this.eventObj[name]) {
// 如果尚未注册,用数组来收集回调函数
this.eventObj[name] = [];
}
this.eventObj[name].push(callback);
}

$emit(name) {
const eventList = this.eventObj[name];
for (const callback of eventList) {
// 执行回调
callback(arguments);
}
}
}

let EB = new EventBus();

// 订阅事件
EB.$on('上课', () => {
console.info("该上课了");
})
EB.$on("上课", () => {
console.info("做笔记");
})
EB.$on("下课", () => {
console.info("下课啦!");
})

// 发布事件
EB.$emit('上课');
EB.$emit('下课');

如何在发布消息时携带参数

1
2
3
4
5
6
7
8
9
10
11
12
13
$on(name, callback) {
if (!this.eventObj[name]) {
this.eventObj[name] = [];
}
this.eventObj[name].push(callback);
}

$emit(name, ...args) {
const eventList = this.eventObj[name];
for (const callback of eventList) {
callback(...args);
}
}

一次订阅

1
2
3
4
5
6
7
8
9
// 添加新的数据结构
this.onceObj = {}; // interface OnceEventObj { [key: string]: Function[] }

$once(eventName, callback) {
if (!this.onceObj[eventName]) {
this.onceObj[eventName] = []
}
this.onceObj[eventName].push(callback)
}

取消订阅

1
2
3
4
5
6
7
8
9
10
11
12
13
14
// 这里引入一个id参数,标识每个回调,同时修改eventObj的结构
// interface EventObj { [key: string]: { [id: number]: Function } }
$off(eventName, id) {
if (id) {
// 传入了id则删除对应的回调
delete this.eventObj[eventName][id];
if (!Object.keys(this.eventObj[eventName]).length) {
delete this.eventObj[eventName];
}
} else {
// 否则删除整个事件的所有回调
delete this.eventObj[eventName];
}
}

清除事件

1
2
3
4
5
6
7
8
9
$clear(eventName: string = '') {
// 未提供事件名称,默认清除所有事件
if (!eventName) {
this.eventObj = {};
return;
}
// 清除指定事件
delete this.eventObj[eventName];
}

终版(TS版本)

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
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
interface EventObj {
[key: string]: { [id: number]: Function }
}

interface OnceEventObj {
[key: string]: Function[]
}

class EventBus {
eventObj: EventObj
/** 每个函数的ID */
callbcakId: number
onceObj: OnceEventObj

constructor() {
this.eventObj = {};
this.callbcakId = 0;
this.onceObj = {};
}

$on(eventName: string, callback: Function) {
if (!this.eventObj[eventName]) {
this.eventObj[eventName] = {};
}
const id = this.callbcakId++;
this.eventObj[eventName][id] = callback;
return id;
}

$emit(eventName: string, ...args: any) {
const eventList = this.eventObj[eventName];
for (const id in eventList) {
typeof eventList[id] === 'function' && eventList[id](...args);
}
const onceEvent = this.onceObj[eventName];
if (onceEvent) {
onceEvent.forEach((callback) => callback(...args));
delete this.onceObj[eventName];
}

}

$off(eventName: string, id: number) {
if (id) {
// 传入了id则删除对应的回调
delete this.eventObj[eventName][id];
if (!Object.keys(this.eventObj[eventName]).length) {
delete this.eventObj[eventName];
}
} else {
// 否则删除整个事件的所有回调
delete this.eventObj[eventName];
}
}

$once(eventName: string, callback: Function) {
if (!this.onceObj[eventName]) {
this.onceObj[eventName] = [];
}
this.onceObj[eventName].push(callback);
}

$clear(eventName: string = '') {
// 未提供事件名称,默认清除所有事件
if (!eventName) {
this.eventObj = {};
return;
}
// 清除指定事件
delete this.eventObj[eventName];
}
}

let EB = new EventBus();

// 订阅事件
EB.$on('上课', (name: string) => {
console.info(`学生${name}到上课地点`);
})
EB.$on("教室上课", (name: string, course: string) => {
console.info(`学生${name}去教室上${course}课`);
})
let id = EB.$on("户外上课", (name: string, course: string) => {
console.info(`学生${name}去户外上${course}课`);
})
EB.$once("献血", (name: string) => {
console.info(`学生${name}去献血`);
})

// 发布事件
EB.$emit('上课', '张三');
EB.$emit('上课', '李四');
EB.$emit('教室上课', '张三', '编程');
EB.$emit('户外上课', '张三', '田径');
EB.$off('户外上课', id);
EB.$emit('户外上课', '李四', '足球');
EB.$emit('献血', '王五');
EB.$emit('献血', '马六');
console.log(EB)
EB.$clear()
console.log(EB)

为什么不被推荐使用

事件总线使用起来非常简单,但是!

  • 随着事件的推移,注册的事件可能越来越多,如果没有及时清理相关事件,整个对象占用的内存会越来越大
  • 当逻辑变得复杂时大量使用事件总线,会让数据流混乱,难以预测,这样在调试代码时难以定位或修改

替代方式

  • 状态提升
    • 有时我们需要在兄弟组件间传递数据,这种情况可以把共享状态提升到最近的共同父组件中去
  • 使用状态管理工具
    • 主流前端开发框架都有相应的状态管理方案,比如redux、mobx、Vuex