发布-订阅模式-- 《Javascript 设计模式与开发实践》

685 阅读5分钟

什么是发布-订阅模式

发布订阅模式,它定义了对象之间的一种一对多的依赖关系,当一个对象的状态发生变化时,所有依赖于它的对象都会接收到通知。

生活中的发布-订阅模式

其实我们生活中也有发布订阅模式的例子,比如小明想买某个楼盘的房子,他会打电话给售楼中心的销售人员,销售告诉他可惜卖完了,不过再过段时间就又有新房出售了,所以过了几天小明又来打电话询问房子的事情,结果新房又售罄了。小明很沮丧。我想现实生活中大家不会这么蠢自己每天打电话去问情况,实际上是小明只需要把联系方式留给销售,销售一有新房出售就立刻发信息通知小明。这样销售也不用每天接上百个重复的电话了。无论是小明,小红。只要在他这里留了联系方式的人,一并把信息全部出去,而且还可以附带一些房子的信息,比如房子面积,价格等。用代码实现功能之前我们先分析下需要做哪些东西。

  1. 首先安排一个发布者,这个例子中应该是售楼的销售人员。
  2. 然后给发布者添加一个缓存列表,用于存放回调函数以便通知订阅者(销售人员手上的购房意向名单)
  3. 最后发布消息的时候,发布者会遍历这个缓存列表,依次出发里面存放的订阅者回调函数(挨个给他们发短信)

用发布订阅模式实现房产订购

下面就用代码实现上面的需求。

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平米"); //只有小红收到了订阅消息

好了,到此关于发布-订阅模式基本完成。当然针对这个模式的实践还有很多,但是其原理都是一致的,多敲两遍代码就会慢慢明白了。下面我们来总结下这个模式的优缺点吧。

发布订阅模式的优缺点

优点

  1. 为时间和对象解耦,不用强耦合。你可以随意的添加订阅者而不受其他因素影响。
  2. 弱化对象之间的联系,可以实现一对多的对象关系,发布者只负责发布消息,不需要关系订阅者的状态。

缺点

  1. 消耗一定的时间和内存,当你订阅了一个消息后,也许这个消息从未发出,但是这个订阅者始终会占用内存。
  2. 对象之间的联系被隐藏在背后,尤其当有多个发布者和订阅者嵌套到一起的时候,调试代码变得不那么容易。

小伙伴们,喜欢的话,赶快在你们的代码中用起来吧。

本文代码内容主要引用自曾探的《Javascript 设计模式与开发实践》