【设计模式系列】之【发布-订阅模式】

前言:要提升代码水平,就绕不开设计模式。之前也有过一些了解,但并没有深入学习。最近准备系统的学习一下设计模式,提高设计,解耦的能力,发现了一本好书《JavaScript设计模式与开发实践》,所以边读边写,把常用的设计模式学习并记录在这里。

发布-订阅模式

    • 定义与介绍
    • 发布-订阅模式的例子
      • 必须先发布再订阅吗?
      • 推模型和拉模型
      • 小结

定义与介绍

发布-订阅模式又叫观察者模式,它定义对象间的一种一对多的依赖关系,当一个对象的状态发生改变时,所有依赖于它的对象都将得到通知。在JavaScript开发中,我们一般使用事件模型来替代传统的发布-订阅模式。

实际上,常用的DOM事件就是发布-订阅模式:

document.body.addEventListener('click', function() {
	alert(2);
}, false)

document.body.click();  // 模拟用户点击

我们监听用户点击document.body的动作,但我们没办法预知用户什么时候点击,所以我们订阅document.body上的click事件。当点击发生时,便会向订阅者发布这个消息。

发布-订阅模式的例子

假设这样一个场景: 很多客人在售楼部咨询买房,但目前还没有开盘,所以客人们把自己的联系方式留在售楼部,等到楼房开盘时,售楼部就会根据留下的联系方式,来通知咨询过的客户。

这就是一个典型的发布-订阅模式的场景。

// 全局发布-订阅对象
let Event = (function() {
	// 订阅者Map, key为订阅内容关键字,value为通知方法的数组。
	let clientList = {}, 
	listen,
	trigger,
	remove;

	// 增加订阅者,传入一个订阅关键字key 和一个通知方法fn
	listen = function(key, fn) {
		if(!clientList[key]) {
			clientList[key] = [];
		}
		clientList[key].push(fn);
	};

	// 发布消息,第一个参数为要发布的订阅关键字key,后边的参数为发布时的传参
	trigger = function() {
		// 取出第一个参数key
		let key = Array.prototype.shift.call(arguments),
		// 获取通知方法数组
		fns = clientList[key];
		if(!fns || fns.length === 0) {
			return false
		}
		// 轮流通知
		for(let i=0; i<fns.length; i++) {
			// 剩下的参数通知时传入
			fn.apply(this, arguments);
		}
	};

	// 取消订阅,传入订阅关键字key和要取消的方法
	remove = function(key, fn) {
		// 获取key对应的方法列表,校验
		let fns = clientList[key];
		if(!fns){
			return false;
		}
		// 若不传入要取消的方法,则为取消该key下的所有方法
		if(!fn) {
			fns && (fns.length = 0);
		} else {
		// 传入时,倒序遍历找到该方法,从数组中删除
			for(let i=fns.length-1; i>=0; i--) {
				if(fns[i] === fn) {
					fns.splice(i, 1);
				}
			}
		}
	}

	return {
		listen,
		trigger,
		remoce,
	}
})();

// 订阅88平米户型
Event.listen('squareMeter88', function() {
	console.log(`price: ${price}`);
})

// 售楼处发布消息
Event.trigger('squareMeter88', 20000)

这样的好处在于:

  1. 不同的客户可以按需求订阅,可以订多种想要的户型,当开盘时都会通知。对于没有订阅的户型,也不会收到垃圾消息。
  2. 当有新的客户需要订阅时,完全不用修改之前的代码,自己使用listen方法把自己加入订阅即可。

必须先发布再订阅吗?

上边的例子中,都是先订阅,发布的时候才会通知。但如果是反过来,还没人订阅的时候就发布了一条消息,那这条消息就消失了。

我们希望能像QQ微信离线消息一样,订阅时可以收到之前发布的消息。为了满足这点,就要建立一个存放离线事件的堆栈,发布时如果还没有订阅者,就先把他储存下来。等终于有对象订阅此事件时,我们再遍历堆栈,重新发布一次,发布之后再从堆栈中删除它。

代码:

// 支持先发布后订阅的 全局发布-订阅对象
let Event = (function() {
	// 订阅者Map, key为订阅内容关键字,value为通知方法的数组。
	let clientList = {}, 
	waitPublish = [], // 储存没有订阅者的发布事件
	listen,
	trigger,
	remove;

	// 增加订阅者,传入一个订阅关键字key 和一个通知方法fn
	listen = function(key, fn) {
		if(!clientList[key]) {
			clientList[key] = [];
		}
		clientList[key].push(fn);
		// 当该key是第一次被订阅时,遍历待发布事件,若有则重新发布
		if(clientList[key].length === 1) {
			for(let i=0; i<waitPublish.length; i++) {
				// 重新发布
				fn.apply(this);
				// 发布后删除
				waitPublish.splice(i, 1);
			}
		}
	};

	// 发布消息,第一个参数为要发布的订阅关键字key,后边的参数为发布时的传参
	trigger = function() {
		// 取出第一个参数key
		let key = Array.prototype.shift.call(arguments),
		// 获取通知方法数组
		fns = clientList[key];
		// 没有订阅者时
		if(!fns || fns.length === 0) {
			// 把该发布事件暂存起来
			let waitFn = function() {
				// 带上参数
				trigger(key, ...arguments);
			}
			waitPublish.push(waitFn);
			return false
		}
		// 轮流通知
		for(let i=0; i<fns.length; i++) {
			// 剩下的参数通知时传入
			fn.apply(this, arguments);
		}
	};

	// 取消订阅,传入订阅关键字key和要取消的方法
	remove = function(key, fn) {
		// 获取key对应的方法列表,校验
		let fns = clientList[key];
		if(!fns){
			return false;
		}
		// 若不传入要取消的方法,则为取消该key下的所有方法
		if(!fn) {
			fns && (fns.length = 0);
		} else {
		// 传入时,倒序遍历找到该方法,从数组中删除
			for(let i=fns.length-1; i>=0; i--) {
				if(fns[i] === fn) {
					fns.splice(i, 1);
				}
			}
		}
	}

	return {
		listen,
		trigger,
		remoce,
	}
})();

推模型和拉模型

推模型: 在事件发生时,发布者一次性把所有的数据和状态变化推送给订阅者。

拉模型:发布者仅仅通知订阅者事件已经发生,订阅者根据一些公开的借口主动拉取自己需要的数据。

拉模型的好处是可以按需获取,但同时有可能让发布者变的门户大开,也增加了代码的复杂度。JavaScript中的arguments可以方便的表示参数列表,所以我们一般都选择推模型,使用apply方法把所有参数都推送给订阅者。

小结

JavaScript中的发布订阅模式跟其他语言有所不同,Java中通常会把订阅者本身传入发布者中,而JavaScript中,我们注册回调函数的形式来代替,更优雅和简单。

发布-订阅模式的优点:

  1. 时间上的解耦,订阅之后不用管,自然会通知
  2. 对象间的解耦,应用广泛。

缺点:

  1. 创建订阅者本身要消耗内存,订阅消息后,也许一直都没有发布,但也会始终存在于内存中。
  2. 虽然弱化了对象间的联系,但过度使用的话,也会导致程序难以跟踪维护。

你可能感兴趣的:(设计模式,前端工程师从初级到高级)