发布-订阅模式

什么是发布-订阅模式

以我们使用过的 documeng.body.addEventListener(“click”,function(){}) 为例,这样的就是发布-订阅模式的具体实现。
我们订阅了 documeng.body 上的 click 事件,当被点击的时候 body 节点便会向订阅者发布者消息。

如果用生活中的例子举例,那就是我们订阅微信公众号,公众号发送消息,订阅的用户就会接收到消息。

基于以上的例子,我们总结出发布订阅模式的三要素:

  • 一个订阅者
  • 一个发布者
  • 一个处理 订阅和发布的中间人

接下来让我们来实现一个发布-订阅模式。
我们规定 listen-订阅 trigger-发布 -remove 取消订阅

一个简单的发布订阅模式

调用的形式如下:

1
2
3
4
5
6
salesOffices.listen(function (price) {
console.log("价格", price);
});

salesOffices.trigger(100);
salesOffices.remove(100);

具体实现如下:

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
var salesOffices = {}; // 定义售楼处
salesOffices.clientList = []; // 缓存列表,存放订阅者的回调函数

salesOffices.listen = function (fn) {
// 新增订阅者
this.clientList.push(fn); // 把订阅的消息添加进缓存列表
};

salesOffices.trigger = function () {
//发布消息
for (let i = 0; i < this.clientList.length; i++) {
this.clientList[i].apply(this, arguments);
}
};
salesOffices.remove = function () {
// 移除所有订阅者
this.clientList.length = 0;
};

salesOffices.listen(function (price) {
console.log("价格", price);
});

salesOffices.listen(function (price) {
console.log("价格", price);
});

salesOffices.remove();

salesOffices.trigger(100);
salesOffices.trigger(50);
// 以上有一个文件如果用户 A 订阅了 100 用户 B 订阅了 50,那么 A 和 B都会收到对方的消息,所以我们还需改进一下,给订阅者添加上唯一的标识

// 也就是把触发的条件改成如下形式

/*
salesOffices.listen("A", function(price) {
console.log("价格", price);
});

salesOffices.trigger("A", 100);

salesOffices.remove("A");
*/

带标识(key)的发布订阅

上一小节中的代码没有订阅标识,一次发布所有用户都会收到,我们改进一下代码让变成谁订阅谁接收

调用方式改成如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
function fn1(price) {
console.log("A价格", price);
}

salesOffices.listen("A", fn1);

salesOffices.listen("A", function () {
console.log("第二个 A");
});

// salesOffices.remove("A", fn1);
salesOffices.remove("A");

salesOffices.listen("B", function (price, area) {
console.log("B 价格", price);
console.log("B 面积", area);
});

salesOffices.trigger("A", 100);
salesOffices.trigger("B", 100, 500);

具体实现如下:

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
var salesOffices = {}; // 定义售楼处
salesOffices.clientList = {}; // 缓存列表,存放订阅者的回调函数

salesOffices.listen = function (key, fn) {
if (!this.clientList[key]) {
// 如果 key 不存在,则新增
this.clientList[key] = [];
}
// 新增订阅者
this.clientList[key].push(fn);
};

salesOffices.trigger = function () {
// 这里获取 key 和 参数
// 疑问? 为什么调用 trigger 时不能直接传入 key 和 参数呢?
// 因为 arguments 是一个类数组,无法调用数组的方法
// 但是如果我们通过 原型链上的方法和改变 this 的指向来调用
let key = Array.prototype.shift.call(arguments);
//发布消息
for (let i = 0; i < this.clientList[key].length; i++) {
// 因为 shift 会改变原数组 所以这里的 arguments 只剩下了除 key 以外的参数了
this.clientList[key][i].apply(this, arguments);
}
};

// 参考 removeEventListener 移除的时候也需要传入 移除的函数
salesOffices.remove = function (key, fn) {
if (this.clientList[key]) {
let arr = this.clientList[key];

if (fn) {
for (let i = 0; i < arr.length; i++) {
if (arr[i].name === fn.name) {
arr.splice(i, 1);
}
}
} else {
// 如果传入的函数为空,则移除所有的函数
this.clientList[key] = [];
}
}
};

function fn1(price) {
console.log("A价格", price);
}

salesOffices.listen("A", fn1);

salesOffices.listen("A", function () {
console.log("第二个 A");
});

// salesOffices.remove("A", fn1);
salesOffices.remove("A");

salesOffices.listen("B", function (price, area) {
console.log("B 价格", price);
console.log("B 面积", area);
});

salesOffices.trigger("A", 100);
salesOffices.trigger("B", 100, 500);

发布订阅的通用实现

在上一节中,我们只是实现了一个发布订阅的功能不够通用,所以我们将继续提取出一个通用的实现,这样我们就可以在不同的地方使用这个功能了

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
class Observer {
constructor() {
this.subscribers = {};
}

listen(key, fn) {
if (!this.subscribers[key]) {
this.subscribers[key] = [];
}
this.subscribers[key].push(fn);
}
trigger() {
const key = Array.prototype.shift.call(arguments);
for (let i = 0; i < this.subscribers[key].length; i++) {
this.subscribers[key][i].apply(this, arguments);
}
}
remove(key, fn) {
if (this.subscribers[key]) {
let arr = this.subscribers[key];
// 如果传入了 fn 就移除指定的 fn
if (fn) {
for (let index = 0; index < arr.length; index++) {
if (arr[index].name === fn.name) {
this.subscribers[key].splice(index, 1);
}
}
} else {
// 没传入 fn 就移除所有的 fn
this.subscribers[key] = [];
}
}
}
}

let observer = new Observer();

function fn(price) {
console.log("fn() A", price);
}

observer.listen("A", function (price) {
console.log("A的价格", price);
});

observer.listen("A", fn);

observer.listen("B", function (price) {
console.log("B的价格", price);
});
observer.remove("A", fn);

observer.trigger("A", 100);

observer.trigger("B", 500);

思维导图

发布订阅模式

参考文章

《JavaScript 设计模式与开发实践》

观察者模式 vs 发布订阅模式,千万不要再混淆了

理解【观察者模式】和【发布订阅】的区别

JavaScript:发布-订阅模式与观察者模式