在事件驱动的环境中,比如浏览器这种持续寻求用户关注的环境中,观察者模式是一种管理人与其任务(确切的讲,是对象及其行为和状态之间的关系)之间的关系的得力工具。用javascript的话来讲,这种模式的实质就是你可以对程序中某个对象的状态进行观察,并且在其发生变化时可以得到通知。
观察者模式中存在俩个角色:观察者和被观察者。也可以叫做发布者和订阅者。这种模式在javascript中有几种不同的实现方法,下面将对其中的一些实现方式进行考察。
模式的实践:
在javascript中有多种方法可以实现发布者-订阅者模式,在展示这些示例之前,我们先确保各种角色的扮演者(对象)及其行为(方法)都已经就绪。
- 订阅者可以订阅和退订。他们还要接收。他们可以在“由人投送”和“自己收取”之间进行选择。
- 发布者负责投送。他们可以在“送出”和“由人取”之间进行选择。
下面是一个发布者和订阅者之间的互动过程的高层示例。
var Publisher = new Observable; var Subscriber = function (news) { //news delivered directly to my proch; } Publisher.subscribeCustomer(Subscriber); Publisher.deliver(‘extre,extre,read all about it!‘); Publisher.unSubscribeCustomer(Subscriber);
在这个模型中可以看出,发布者处于明显的主导。他们负责登记其客户,而且有权停止投送。最后新的报纸出版后它们会将其投送给客户。
上面的代码创建了一个新的可观察对象(Observable)。它有三个实例方法:subscribeCustomer、unSubscribeCustomer和 deliver。subscribeCustomer方法以一个订阅者的回调函数为参数。deliver方法在调用过程中将通过这些回调函数把数据发送给每一个订阅者。
下面的例子处理同一类问题,但是发布者和订阅者之间的互动方式有所不同。
var newYorkTime = new Publisher; var AustinHerald = new Publisher; var SfChronicle = new Publisher; var Jon = function (from) { console.log(‘Delivery from‘+from+‘to jon‘); } var Lindsay = function (from) { console.log(‘Delivery from‘+from+‘to Lindsay‘); } var Quadaras = function (from) { console.log(‘Delivery from‘+from+‘to Quadaras‘); } jon. subscribe(newYorkTime). subscribe(SfChronicle); Lindsay. subscribe(newYorkTime). subscribe(AustinHerald). subscribe(SfChronicle); Lindsay. subscribe(newYorkTime). subscribe(SfChronicle); newYorkTime.deliver(‘Here is your paper!‘); AustinHerald.deliver(‘News‘).deliver(‘REviews‘).deliver(‘Coupons‘); SfChronicle.deliver(‘the weather is sill chilly‘).deliver(‘hello..‘);
在这个例子中,发布者的创建方式和订阅者接收数据的方式没有多少改变,但是用于订阅和退订权的一方变成了订阅者,当然,负责发送数据的还是发布者一方。
本类中的发布者是 Publisher 的实例。他有一个deliver方法。而作为订阅者的函数对象则拥有subscribe和unsubscribe俩个方法。订阅者只是普通的回调函数,这俩个方法是通过扩展Function的prototype而加入的,下面我们将一步一步的构建符合需要的API。
构建观察者API:
function Publisher(){ this.subscribe = []; }
所有Publisher实例都应该能够投送数据。只要把deliver方法添加到Publisher的prototype中,他就能被所有Publisher对象共享:
Publisher.prototype.deliver = function (data) { this.subscribes.forEach( function (fn) { fn(data); } ); return this; }
这个方法使用的是javascript 1.6中新加的数组方法forEach逐一处理每一个订阅者。forEach方法会对一个“草垛”(haystack)从头到尾访问一遍,把每一根针,针的索引和整个数组提供给一个回调方法。订阅者数组中的每根针都是一个回调函数,比如Joe,Lindsay,Quadaras。
deliver 方法把this用作返回值,因此可以对该方法进行链式调用。
下一步是给以订阅者订阅的能力。
Function.prototype.subscribe = function (publisher) { var _this = this; var alreadyExists = publisher.subscribes.some( function (el) { return el===_this; } ); if (!alreadyExists) { publisher.subscribes.push(this); } return this; }
这段代码为Function的prototype添加了一个以Publisher对象为参数的subscribe方法。所有函数都能调用这个方法。subscribe方法先定义了一个this变量,并把this赋给它。后面用作数组的some方法参数的那个匿名函数将通过闭包机制访问到这个变量,从而访问到用以调用subscribe方法的那个函数对象。
some 也是javascript 1.6中新增加的数组方法,它以一个回调函数为参数,some逐一访问数组的各个元素,并以其为参数调用那个回调函数,只要至少有一次调用函数时返回true,则some方法返回ture,否则some方法返回false。subscribe把some方法的返回值赋值给alreadyExists,然后根据这个变量的值决定是否为指定的发布者添加一个订阅者。最后subscribe方法返回this,支持链式调用。
unsubscribe方法可供订阅者用来停止对事件发布者的观察:
Function.prototype.unsubscribe = function (publisher) { var _this = this; publisher.subscribes = publisher.subscribes.filter( function (el) { return el !== _this; } ); return this; }
有订阅者在监听到某种一次性的事件之后会在回调阶段立即退订该事件。其做法大致如下:
var publisherObject = new Publisher(); var observerObject = function (data) { //process data console.log(data); //unsubscribe from this publisher arguments.callee.unsubscribe(publisherObject); } observerObject.subscribe(publisherObject);
在现实世界中,观察者模式对于那种由许多javascript程序员合作开发的大型程序特别有用。它可以提高API的灵活性,使并行开发的多个实现能够彼此独立地进行修改。作为开发人员,你可以对自己的应用程序中什么是“令人感兴趣的时刻”做出决定。你所监听的不再是click、load、blur和mouseover等浏览器事件,在富用户界面应用程序中,drag,dropmoved,complete和tabSwitch都可能是令人感兴趣的事件。他们都是在普通浏览器事件的基础上抽象出来的可观察事件,可由发布者对象向其监听者广播。
实例动画:
动画是在应用程序中实现可观察对象的一个很好的起点。眨眼之间你就可以想出至少3个可观察的时刻:开始、结束和进行中。在本例中,我们将分别称之为onStart、onComplete、onTween。下面的代码演示了用前面编写的Publisher工具实现这些事件的过程。
var Animation = function (o) { this.onStart = new Publisher, this.onComplete = new Publisher, this.onTween = new Publisher; } Animation.method(‘fly‘, function () { //begin animation this.onStart.deliver(); for (...) { //loop through frames //deliver frame number this.onTween.deliver(i); } //end animation this.onComplete.deliver(); }); //setup an account with the animation manager var Superman = new Animation({config}); //Begin implementing subscribers var putOnCape = function (i) {} var takeOffCape = function (i) {} putOnCape.subscribe(Superman.onStart); takeOffCape.subscribe(Superman.onComplete); //fly can be called anywhere Superman.fly(); //for instance: addEvent(element, ‘click‘, function () { Superman.fly(); });
可以看到,如果你是负责实现为超人披上斗篷和解下斗篷的功能的人的话,这种运作方式还真不错。借助于发布者,你可以知道超人什么时候起飞以及什么时候回到地面,你只需预定这些时刻的通知便万事大吉了。
在DOM脚本编程环境中的高级事件模式中,事件监听器说到底是一种内置的观察者。事件处理器handler与事件监听器listener并不是一回事。前者说穿了就是一种把事件传给与其相关联的函数的手段,而且在这种模型中一种事件只能制定一个回调方法。而在监听器模式中,一个事件可以与几个监听器关联。每个监听器都能独立于其他监听器而改变。打个比方,对SanFrancisco Chronicle这家报社来说 ,其订阅者joe定没有定New York Times都无所谓。同样,joe不在在乎Lindsay是否也订阅了该报纸。每一方都只管处理自己的数据和相关行为。
例如,使用事件监听器,可以让多个函数响应同一个事件:
var el = $(‘example‘); var fn1 = function(e){} var fn2 = function(e){} addEvent(el,‘click‘,fn1) addEvent(el,‘click‘,fn2)
但是事件处理器就办不到
var el = $(‘example‘); var fn1 = function(e){} var fn2 = function(e){} el.onclick = fn1; el.onclick = fn2;
第一个例子中,使用事件监听器,所以fn1和fn2都会被调用。而第二个例子则第二次对onclick赋值会覆盖掉第一次,只会调用fn2。
言归正传,监听器和观察者之间的共同之处显而易见,实际上他们互为同义语。他们都订阅特定的事件,然后等待事件的发生,事件发生的时候,订阅方的回调函数会得到通知。传给他们的参数是一个事件对象,其中包含着事件发生时间,事件类型和事件发源地等有用的信息。
观察者模式适用场合:
如果希望把人的行为和应用程序的行为分开,那么观察者模式正适合这种场合。最好不要实现一些与用户操作绑定在一起而且来源于浏览器的东西,比如 click,keypress之类的基本DOM事件。对于那些只关心动画的开始,或者发现错别字的程序员而言,那些事件提供不了什么有用信息。
举例来说,用户点击导航的一个标签tab时,会打开一个包含更多信息的菜单。当然你可以直接监听这个click,不过这需要知道监听的是哪个元素,这样做的另一个弊端是你的实现与click事件直接绑在了一起。比监听click更好的做法是:创建一个可观察的onTabChange对象。并且在特定事件发生时通知所有观察者。如果菜单改为在鼠标指向标签时或者标签处于焦点之下时打开,那么这个onTabChange对象会替你处理这种改变。
观察者模式是开发基于行为的大型应用程序的有力手段。在一次浏览器会话期间,应用程序中可能会断断续续发生几十次事件,你可以削减为事件注册监听器的次数,让可观察对象借助一个事件监听器替你处理各种行为并将信息委托delegate给她的所有订阅者。从而降低内存消耗和提高性能。这样一来,就不用没完没了地为同样的元素添加新的事件监听器。
创建可观察对象会带来加载时间的开销。但是这可以采用惰性加载技术化解。具体来说就是把新的可观察对象的实例化推迟到需要发送事件通知的时候,这样一来,订阅者在事件尚未创建的时候就能订阅它,应用程序的初始加载时间也就不会受到影响。