JavaScript设计模式与开发实践(五)发布—订阅模式

发布—订阅模式

它定义对象间的一种一对多的依赖关系,当一个对象的状态发生改变时,所有依赖于它的对象都将得到通知。在 JavaScript 开发中,可以用事件模型来实现。

订阅—发布

怎样实现一个简易的发布订阅模式呢?

  1. 首先我们需要一个发布者,订阅者通过调用发布者提供的函数订阅某个类型的消息、消息更新后获取消息。
  2. 同时我们在JavaScript中采事件模型的方式来实现,那么我们需要一个存储订阅者回调函数的地方。
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
class Event {
constructor() {
this.clientMap = {};
}
listen(key, fn) {
if (!this.clientMap[key]) {
this.clientMap[key] = [];
}
this.clientMap[key].push(fn);
}
emit(key, ...args) {
const fns = this.clientMap[key];
if (!fns || fns.length === 0) {
return false;
}
fns.forEach((fn) => {
fn.apply(this, args);
});
}
remove(key, fn) {
const fns = this.clientMap[key]
if (!fn) {
fns && (this.clientMap[key].length = 0)
}
fns.forEach((item, index) => {
if (fn === item || fn.name === item.name || fn === item.fn) {
fns.splice(index, 1)
}
})
}
// 只订阅一次
once(key, fn) {
// 先绑定,调用后删除 我们不直接给事件绑定传入的回调 而是在外面包一层
// on.fn = fn 这行代码是为了在取消订阅中进行判断。
let _this = this;
function on() {
_this.remove(key, on);
fn.apply(_this, arguments);
}
on.fn = fn;
_this.emit(key, on);
return _this;
}
}

const MessageEvent = new Event();

// 订阅
MessageEvent.listen('create', function create(name) {
console.log('event', name);
})

// 发布
MessageEvent.emit('create', 'notify')

// 删除订阅
MessageEvent.remove('create', function create(name) {
console.log('event', name);
})

MessageEvent.emit('create', 'notify')

发布—订阅

正常情况下我们是先订阅再发布,但是某些场景可能我们一开始并没有进行订阅,那这个时间段的消息就不会被收集,当我们订阅后自然也无法收到这些消息。因此我们需要一个缓存的数组,将消息缓存下来,当有订阅时,将缓存的消息发布。

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
class Event {
constructor() {
this.clientMap = {};
this.cacheMap = {};
}
listen(key, fn) {
if (!this.clientMap[key]) {
this.clientMap[key] = []
}

this.clientMap[key].push(fn);

// 有离线事件时,需要将缓存的离线事件也执行
if (this.cacheMap[key] && this.cacheMap[key].length) {
this.cacheMap[key].forEach(cacheFn => {
cacheFn.call(this, key)
});
this.cacheMap[key] = null;
}
}
emit(key, ...args) {
const fns = this.clientMap[key];
if (!fns || fns.length === 0) {
// 如果没有北订阅 需要将事件缓存下来,等有订阅的时候发布
if (!this.cacheMap[key]) {
this.cacheMap[key] = []
}
constcacheFn = () => {
returnthis.emit(key, ...args)
}
this.cacheMap[key].push(cacheFn)
return false;
}

fns.forEach((fn) => {
fn.apply(this, args);
});
}
remove(key, fn) {
const fns = this.clientMap[key]
if (!fn) {
fns && (this.clientMap[key].length = 0)
}
fns.forEach((item, index) => {
if (fn === item || fn.name === item.name || fn === item.fn) {
fns.splice(index, 1)
}
})
}
// 只订阅一次
once(key, fn) {
// 先绑定,调用后删除 我们不直接给事件绑定传入的回调 而是在外面包一层
// on.fn = fn 这行代码是为了在取消订阅中进行判断。
let _this = this;
function on() {
_this.remove(key, on);
fn.apply(_this, arguments);
}
on.fn = fn;
_this.emit(key, on);
return _this;
}
}

const MessageEvent = new Event();

// 发布
MessageEvent.emit('create', 'notify')

// 订阅
MessageEvent.listen('create', function create(name) {
console.log('event', name);
})

// 订阅
MessageEvent.listen('beforeCreated', function beforeCreated(name) {
console.log('beforeCreated', name);
})

MessageEvent.emit('beforeCreated', 'notify')

小结

在JavaScript很多类库中都能看到发布订阅模式的思想。例如:

  1. Vue的事件总线(Event Bus 父子组件通讯)
  2. Vue2的响应式实现(Observer Dep Watcher类实现的观察者模式)
  3. Mobx的发布订阅 @observer
  4. Socket.io
  5. ……

JavaScript设计模式与开发实践(五)发布—订阅模式
https://jing-jiu.github.io/jing-jiu/2023/01/08/notebooks/JavaScript设计模式与实践/设计模式(五)/
作者
Jing-Jiu
发布于
2023年1月8日
许可协议