什么是发布-订阅模式
发布订阅模式,它定义了对象之间的一种一对多的依赖关系,当一个对象的状态发生变化时,所有依赖于它的对象都会接收到通知。
生活中的发布-订阅模式
其实我们生活中也有发布订阅模式的例子,比如小明想买某个楼盘的房子,他会打电话给售楼中心的销售人员,销售告诉他可惜卖完了,不过再过段时间就又有新房出售了,所以过了几天小明又来打电话询问房子的事情,结果新房又售罄了。小明很沮丧。我想现实生活中大家不会这么蠢自己每天打电话去问情况,实际上是小明只需要把联系方式留给销售,销售一有新房出售就立刻发信息通知小明。这样销售也不用每天接上百个重复的电话了。无论是小明,小红。只要在他这里留了联系方式的人,一并把信息全部出去,而且还可以附带一些房子的信息,比如房子面积,价格等。用代码实现功能之前我们先分析下需要做哪些东西。
- 首先安排一个发布者,这个例子中应该是售楼的销售人员。
- 然后给发布者添加一个缓存列表,用于存放回调函数以便通知订阅者(销售人员手上的购房意向名单)
- 最后发布消息的时候,发布者会遍历这个缓存列表,依次出发里面存放的订阅者回调函数(挨个给他们发短信)
用发布订阅模式实现房产订购
下面就用代码实现上面的需求。
const salesOffices = {}; // 定义发布者
salesOffices.clientList = []; // 创建缓存列表
salesOffices.addListener = function(fn) { // 添加订阅者
this.clientList.push(fn);
}
salesOffices.trigger = function(){ // 发布消息
this.clientList.forEach((item, index) => {
item.apply(this, arguments);
})
}
salesOffices.addListener(function(price, sqaureMeter) { // 小明订阅消息
console.log('price is:', price)
console.log('sqaureMeter is:', sqaureMeter)
})
salesOffices.addListener(function(price, sqaureMeter) { // 小红订阅消息
console.log('price is:', price)
console.log('sqaureMeter is:', sqaureMeter)
})
salesOffices.trigger(8000, "100平米");
salesOffices.trigger(4000, "90平米");
至此,我们实现了一个最简单的发布-订阅模式,但是这里还存在一些问题,假设小明只想买90平及以下的房子,那么他也收到了100平房子的消息,显然这是不合理的。所以现在我们有必要增加一个标识key,让订阅者只订阅自己感兴趣的消息。那我们把代码稍微改动下。
const salesOffices = {}; // 定义发布者
salesOffices.clientList = {}; // 创建缓存列表
salesOffices.addListener = function (key, fn) { // 添加订阅者
if (!this.clientList[key]) {
this.clientList[key] = [];
}
this.clientList[key].push(fn);
}
salesOffices.trigger = function () { // 发布消息
const subKey = Array.prototype.shift.call(arguments);
const fns = this.clientList[subKey];
if (!fns || fns.length === 0) {
return false;
}
fns.forEach((fn) => {
fn.apply(this, arguments);
});
}
salesOffices.addListener("sqaureMeter90", function (price, sqaureMeter) { // 小明订阅消息
console.log('price is:', price)
console.log('sqaureMeter is:', sqaureMeter)
})
salesOffices.addListener("sqaureMeter100", function (price, sqaureMeter) { // 小红订阅消息
console.log('price is:', price)
console.log('sqaureMeter is:', sqaureMeter)
})
salesOffices.trigger("sqaureMeter100", 8000, "100平米");
salesOffices.trigger("sqaureMeter90", 4000, "90平米");
现在用户只会收到订阅自己感兴趣的消息了。
抽象发布订阅功能
现在这个功能基本好用了,但是发布订阅是比较通用的功能,可能也会用在其他地方,现在我们把它抽象成一个单独的对象。
const Event = {
clientList: {},
addListener: function (key, fn) { // 添加订阅者
if (!this.clientList[key]) {
this.clientList[key] = [];
}
this.clientList[key].push(fn);
},
trigger: function () { // 发布消息
const subKey = Array.prototype.shift.call(arguments);
const fns = this.clientList[subKey];
if (!fns || fns.length === 0) {
return false;
}
fns.forEach((fn) => {
fn.apply(this, arguments);
});
}
}
然后再用上面的代码测试下
const salesOffices = Object.create(Event, {}); // 定义发布者
salesOffices.addListener("sqaureMeter90", function (price, sqaureMeter) { // 小明订阅消息
console.log('price is:', price)
console.log('sqaureMeter is:', sqaureMeter)
})
salesOffices.addListener("sqaureMeter100", function (price, sqaureMeter) { // 小红订阅消息
console.log('price is:', price)
console.log('sqaureMeter is:', sqaureMeter)
})
salesOffices.trigger("sqaureMeter100", 8000, "100平米");
salesOffices.trigger("sqaureMeter90", 6000, "90平米");
结果都是一样的。
取消订阅的事件
如果哪一天小明买到房子了或者不想买了,他就不用继续订阅消息了,所以我们应该再添加一个可以取消订阅的功能。
Event.remove = function (key, fnName) {
const fns = this.clientList[key];
if (!fns || fns.length === 0) {
return false;
}
if (!fnName) { // 如果没有传入对应的回调函数名,则认为移除所有的订阅
this.clientList[key] = [];
} else {
fns.forEach((fn, index) => {
if (fn === fnName) {
fns.splice(index, 1);
}
})
}
}
const salesOffices = Object.create(Event, {}); // 定义发布者
const mingSbuscrible = function (price, sqaureMeter) { // 小明订阅消息
console.log('ming price is:', price)
console.log('ming sqaureMeter is:', sqaureMeter)
}
const hongSubscrible = function (price, sqaureMeter) { // 小红订阅消息
console.log('hong price is:', price)
console.log('hong sqaureMeter is:', sqaureMeter)
}
salesOffices.addListener("sqaureMeter100", mingSbuscrible)
salesOffices.addListener("sqaureMeter100", hongSubscrible)
salesOffices.remove("sqaureMeter100", mingSbuscrible) //小明移除了消息订阅
salesOffices.trigger("sqaureMeter100", 8000, "100平米"); //只有小红收到了订阅消息
好了,到此关于发布-订阅模式基本完成。当然针对这个模式的实践还有很多,但是其原理都是一致的,多敲两遍代码就会慢慢明白了。下面我们来总结下这个模式的优缺点吧。
发布订阅模式的优缺点
优点
- 为时间和对象解耦,不用强耦合。你可以随意的添加订阅者而不受其他因素影响。
- 弱化对象之间的联系,可以实现一对多的对象关系,发布者只负责发布消息,不需要关系订阅者的状态。
缺点
- 消耗一定的时间和内存,当你订阅了一个消息后,也许这个消息从未发出,但是这个订阅者始终会占用内存。
- 对象之间的联系被隐藏在背后,尤其当有多个发布者和订阅者嵌套到一起的时候,调试代码变得不那么容易。
小伙伴们,喜欢的话,赶快在你们的代码中用起来吧。
本文代码内容主要引用自曾探的《Javascript 设计模式与开发实践》