观察者模式(Observer Pattern),也被称为“发布/订阅模型(publisher/subscriber model)”。在这种模式中,有两类对象,分别是“观察者-Observer”和“目标对象-Subject”。目标对象中保存着一份观察者的列表,当目标对象的状态发生改变的时候就主动向观察者发出通知(调用观察者提供的方法),从而建立一种发布/订阅的关系。这一种发布/订阅的关系常用于实现事件、消息的处理系统。
在我们的生活中,也存在着许多观察者模式,最简单的例子就是“微博”。关注和被关注的关系,其实就是一个发布/订阅模型。假如,方舟子“悄悄关注”了天才韩寒,韩寒在微博上每发出一条消息都会反馈到方舟子的消息列表中,方舟子便可端坐家中,阴阴一笑,“嘿嘿,小子你干了什么我都知道……”,然后方舟子就开始行动了。
我们先看看传统的观察者模式是怎样的吧(Java版):
//被观察者 public class Subject{ private Array obArray = new Array(); //增加一个观察者 public void addObserver(Observer o){ this.obArray.add(o); } //删除一个观察者 public void removeObserver(Observer o){ this.obArray.remove(o); } //通知所有观察者 public void notifyObservers(){ for(Observer o: this.obArray){ o.update(); } } public void doSomething(){ //更新状态,告诉所有观察者 this.notifyObservers(); } } //观察者 public class Observer{ public void update(){ //目标对象更新了,要做点什么了 } }
使用观察者模式的好处是,当一个对象A需要通知另外一个对象B的时候,无需在A中声明B,在保持相关对象行为一贯性的时候,避免对象之间的紧耦合,这样可以使对象有很好的重用性。
JavaScript是一个事件驱动型语言,观察者模式可谓随处可见,例如:
document.body.onclick = function(){ alert('我是一个观察者,你一点击,我就知道了'); } //或者是 document.body.addEventListener('click',function(){ alert('我也是一个观察者,你一点击,我就知道了'); });
这个例子中的发布/订阅关系是由JavaScript语言本身实现的,DOM的每个节点都可以作为Subject,提供了很多事件处理(Event handle)的接口,你只需要给这些接口添加监听函数(也就是Observer),就可以捕获触发的事件进行处理。
然而在我们自己写的对象中,要实现这种发布/订阅的关系,就需要自己来实现这个观察者模型,例如:
var ObserverPattern= function(){ //基于事件的观察者列表 this.eventObsArray = {}; } ObserverPattern.prototype = { //通知某个事件的所有观察者 notifyObservers: function(eventName,datas){ var observers= this.eventObsArray[eventName]||[],i,ob; for(i=0;ob=observers[i];i++){ ob.handler.apply(ob.scope,datas||[]); } }, //给某个事件添加观察者 addObserver: function(eventName,handleFunction,observer){ var events = this.eventObsArray, events[eventName] = events[eventName]||[]; events[eventName].push({ //传入的observer参数是handleFunction中的this scope: observer || this, handler: handleFunction }); }, //取消某个观察者对某事件的观察 removeObserver: function(eventName,observer){ var evts = this.eventObsArray; if(!evts[eventName]) return; evts[eventName]=evts[eventName].filter(function(ob){ return ob.scope!=observer; }); } } var 韩寒 = new ObserverPattern(); var 方舟子 = { doSomeResearch: function(){alert('嘿嘿…我在搞研究…')} } //韩寒一写博客,方舟子就开始研究了 韩寒.addObserver('写博客',function(){ this.doSomeResearch(); },方舟子);
然而这种形式的发布/订阅模型,还是有些不足的地方,整个关系链条是由目标对象维护的,观察者无法主动去监听目标对象的变化;其次,观察者不知道其他观察者的存在,有时一个观察者的处理有时还会触发其他的事件,无法让其他观察者进行后续处理。
方舟子观察韩寒,难道韩寒就不可以看看方舟子了?其实,目标对象也可以是观察者,咱们对上面的ObserverPattern再改进改进:
var ObserverPattern= function(obj){ for(var i in obj){ this[i] = obj[i]; } this.eventObsArray = {}; } ObserverPattern.prototype = { //监听某个目标对象 listen: function(subject, eName, handler){ subject.addObserver(eName, handler, this) }, //取消监听某个目标对象 ignore: function(subject, eName){ subject.removeObserver(eName,this); }, //之前定义的方法,这里就不多说了 notifyObservers: function(eName,datas){}, addObserver: function(eName,handler,ob){}, removeObserver: function(eName,ob){} } var 韩寒 = new ObserverPattern({ postReward: function(){alert('研究吧, 奖金2000万…')}, writeBlog: function(){this.notifyObservers('写博客')} }); var 方舟子 = new ObserverPattern({ doSomeResearch: function(){ alert('嘿嘿…我在搞研究…'); this.notifyObservers('搞研究') } }); //韩寒一发微博,方舟子就开始研究了 方舟子.listen(韩寒,'写博客',方舟子.doSomeResearch); //方舟子一开始研究,韩寒就发赏金了 韩寒.listen(方舟子,'搞研究',韩寒.postReward);
同时,当把目标对象和观察者整合到一起的时候,就形成了一条事件的触发链,一个事件可以触发另一个事件,一个观察者可以将自己观察的结果告诉其他观察者。当然,也要小心事件的循环促发,或者像”蝴蝶效应”那样让一个无关紧要的事件产生过大的影响。
上面的ObserverPattern已经相对完善了,但是使用起来还是有不少限制。例如,需要保证目标对象和观察者先被创建才被调用;一个事件只能被一个目标对象触发,无法一个事件监听多个消息来源。虽然这些也不算什么大问题,但是还有一种更加灵活的方式来管理我们的事件。
//全局的事件监听模块,可用于对象之间的消息传递 var Event = (function(){ var events = {}, registerEvent = function(eName, handler, scope){ events[eName] = events[eName] || []; events[eName].push({ scope: scope || this, handler: handler }); }, removeEvent = function(eName, handler, scope){ scope = scope || this; if(!fns) return; events[eName] = events[eName].filter(function(fn){ return fn.scope!=scope || fn.handler!=handler }); }, triggerEvent = function(eventName,params){ var fns = events[eventName],i,fn; if(!fns) return; for(i=0;fn=fns[i];i++){ fn.handler.apply(fns.scope,params||[]); } }; return { listen: registerEvent, ignore: removeEvent, trigger: triggerEvent } })(); Event.listen('韩寒写博客', 方舟子.doSomeResearch, 方舟子); (function(){ alert('我是路人甲,我告诉方舟子,韩寒写博客了'); Event.trigger('韩寒写博客'); })();
这种方式的确更为灵活,但越是灵活就越是不好把握,这是一把双刃剑,要小心使用。这种情况下,观察者与目标对象之间的依存关系是很难被跟踪的,很容易像“蝴蝶效应”那样产生意想不到的结果。
最后说一下,韩粉方粉别太在意,我不是故意拿你们教主来开刷的,只是碰巧这样很形象嘛 ~