JavaScript设计模式-观察者模式 (发布/订阅模式)

449 阅读5分钟

观察者模式又成为订阅发布模式。

目的:解耦各参与者,让各参与者只关注自己应该实现的操作,各参与者通过消息来相互协作。主要用于实现事件机制。

实现:订阅者把自己的某个函数注册到发布者中,该函数通常叫做监听函数(Listener)。当发布者运行到某个事件的时候,回调注册的监听函数。订阅者即收到了发布者发送的通知。

职能: 1)发布者:提供订阅接口,让订阅者可以订阅事件;发布事件。2) 订阅者:对感兴趣的发布者,注册监听函数。


定义对象间的一种一对多依赖关系,使得每当一个对象状态发生改变时,其相关依赖对象皆得到通知并被自动更新。

现实中最常见的最简单的情况

绑定DOM事件就是一种发布订阅模式。 监控用户点击document.body的动作,但是我们没办法预知用户将在什么时候点击。所以我们订阅document.body上的click事件,当body节点被点击时,body节点便会向订阅者发布这个消息。这很像购房的例子,购房者不知道房子什么时候开售,于是他在订阅消息后等待售楼处发布消息。

        //订阅 
        document.body.addEventListener('click', function() {
	    console.log('click!');
	}, false);
        
        //发布
	document.body.click(); 

自定义事件

1)指定好发布者;
2)发布者有一个缓存列表,里面存放了回调函数,以便发布后通知订阅者;
3)发布消息的时候遍历缓存列表,依次触发订阅者的回调;


通用实现 

功能:为全局对象 Event 作为一个类似 ‘中介者’,把订阅者跟发布者联系起来。

优点:一是时间上的解耦(异步),二是空间上的解耦; 

注意:Event 作为全局对象,处理页面的较大事务时,建议创建命

使用一个全局的Event对象(唯一一个):

           var Event = (function() {
		var clientList = {},  
			listen,
			trigger,
			remove;

		remove = function(key, fn) {
			var fns = this.clientList[key];
			if (!fns) return false;
			if (!fn) return fns.length = 0;
			for (var i = fns.length - 1; i >= 0; i--) {
				if (fn == fns[i]) {
					fns.splice(i, 1);
				}
			}
		};

		trigger = function() {
			var key = Array.prototype.shift.call(arguments),
			  	fns = clientList[key];
			if (!fns || !fns.length) {
				return false;
			}
			for (var i = 0, fn; fn = fns[i++];) {
				fn.apply(this, arguments);
			}
		};

		listen = function(key, fn) {
			if (!clientList[key]) {
				clientList[key] = [];
			}
			clientList[key].push(fn);
		};
		return {
			listen: listen,
			trigger: trigger,
			remove: remove
		};
	})();

注:我们给每个发布者对象都添加了listen和trigger方法,以及一个缓存列表clientList,这其实是一种资源浪费。

必须知道发布者的名字叫salesOffices,一旦想订阅另外一个发布者,我们得再粘一次代码。

发布—订阅模式可以用一个全局的Event对象来实现,订阅者不需要了解消息来自哪个发布者,发布者也不知道消息会推送给哪些订阅者,Event作为一个类似中介者的角色,把订阅者和发布者联系起来

案例

html 代码

 <button id="my">点我</button>
 <div id="showCount"></div>

js代码

        var count = 0;
	var oMy = document.getElementById("my");
	oMy.addEventListener("click",function() {
                //发布
		Event.trigger('add',count++ )
	})
	var showCount = document.getElementById("showCount")
	Event.listen('add',function() {
                //订阅
		showCount.innerHTML = count
	})

建立一个存放离线事件的堆栈,当事件发布的时候,如果此时还没有订阅者来订阅这个事件,我们暂时把发布事件的动作包裹在一个函数里,这些包装函数将被存入堆栈中,等到终于有对象来订阅此事件的时候,我们将遍历堆栈并且依次执行这些包装函数,也就是重新发布里面的事件。当然离线事件的生命周期只有一次,就像QQ的未读消息只会被重 新阅读一次,所以刚才的操作我们只能进行一次。


封装

        function installEvent (obj) {
		for(var i in Event) {
			obj[i] = Event[i]
		}
		console.log(obj)
	}


案例

        var login = {}
	function installEvent (obj) {
		for(var i in Event) {
			obj[i] = Event[i]
		}
		console.log(obj)
	}
	installEvent(login)

        //订阅
	login.listen('loginSucc', function(data) {
		if(data.isLogin == "1") {
		    console.log("已经登录")
		}
	});

        //发布
	login.trigger("loginSucc",{
		isLogin: "1"
	});

总结

发布—订阅模式的优点非常明显,一为时间上的解耦,二为对象之间的解耦。它的应用非常广泛,既可以用在异步编程中,也可以帮助我们完成更松耦合的代码编写。

发布—订阅模式还可以用来帮助实现一些别的设计模式,比如中介者模式。从架构上来看,无论是 MVC 还是 MVVM, 都少不了发布—订阅模式的参与,而且JavaScript本身也是一门基于事件驱动的语言。  

当然,发布—订阅模式也不是完全没有缺点(浪费内存)。创建订阅者本身要消耗一定的时间和内存,而且当你订阅一个消息后,也许此消息最后都未发生,但这个订阅者会始终存在于内存中。另外,发布—订阅模式虽然可以弱化对象之间的联系,但如果过度使用的话,对象和对象之间的必要联系也将被深埋在背后,会导致程序难以跟踪维护和理解。特别是有多个发布者和订阅者(b订阅a的消息并发布给c)嵌套到一起的时候,要跟踪一个bug不是件轻松的事情。


知识点

Array.prototype.shift.call(arguments)

arguments是一个类数组对象,虽然有下标,但不是真正的数组,没有shift方法,这时可以通过call或者apply方法调用Array.prototype中的shift方法。

shift 方法 移除数组中的第一个元素并返回该元素