本文共 13102 字,大约阅读时间需要 43 分钟。
本节书摘来自异步社区《JavaScript设计模式》一书中的第9章,第9.5节, 作者: 【美】Addy Osmani 译者: 徐涛 更多章节内容可以访问云栖社区“异步社区”公众号查看。
Observer(观察者)是一种设计模式,其中,一个对象(称为subject)维持一系列依赖于它(观察者)的对象,将有关状态的任何变更自动通知给它们(见图9-3)。
当一个目标需要告诉观察者发生了什么有趣的事情,它会向观察者广播一个通知(可以包括与通知主题相关的特定数据)。
当我们不再希望某个特定的观察者获得其注册目标发出的改变通知时,该目标可以将它从观察者列表中删除。
参考之前发布的设计模式定义通常是很有用的,它与语言无关,以便久而久之使其使用和优势变得更有意义。“四人组”所著书籍(《设计模式:可复用面向对象软件的基础》)中提供的Observer模式的定义是:
“一个或多个观察者对目标的状态感兴趣,它们通过将自己依附在目标对象上以便注册所感兴趣的内容。目标状态发生改变并且观察者可能对这些改变感兴趣,就会发送一个通知消息,调用每个观察者的更新方法。当观察者不再对目标状态感兴趣时,它们可以简单地将自己从中分离。”
现在我们可以扩展所学到的内容来使用以下组件实现Observer模式:Subject(目标)
维护一系列的观察者,方便添加或删除观察者
Observer(观察者)
为那些在目标状态发生改变时需获得通知的对象提供一个更新接口
ConcreteSubject(具体目标)
状态发生改变时,向Observer发出通知,储存ConcreteObserver的状态
ConcreteObserver(具体观察者)
存储一个指向ConcreteSubject的引用,实现Observer的更新接口,以使自身状态与目标的状态保持一致
首先,让我们来模拟一个目标可能拥有的一系列依赖Observer:
function ObserverList(){ this.observerList = [];}ObserverList.prototype.Add = function(obj){ return this.observerList.push(obj);};ObserverList.prototype.Empty = function(){ this.observerList = [];};ObserverList.prototype.Count = function(){ return this.observerList.length;};ObserverList.prototype.Get = function (index){ if (index > -1 && index
// 使用extension扩展对象
function extend(obj, extension){ for (var key in obj){ extension[key] = obj[key]; }}
接下来,让我们来模拟目标(Subject)和在观察者列表上添加、删除或通知观察者的能力。
function Subject() { this.observers = new ObserverList();}Subject.prototype.AddObserver = function(observer){ this.observers.Add(observer);};Subject.prototype.RemoveObserver = function (observer){this.observers.RemoveIndexAt(this.observers.IndexOf(observer, 0));};Subject.prototype.Notify = function (context){ var observerCount = this.observers.Count(); for (var i = 0; i < observerCount; i++){ this.observers.Get(i).Update(context); }};
然后定义一个框架来创建新的Observer。这里的Update功能将在后面的自定义行为部分进一步介绍。
// The Observerfunction Observer(){ this.Update = function (){ // ... };}
在使用上述Observer组件的样例应用程序中,定义如下:
用于向页面添加新可见checkbox的按钮
控制checkbox,将充当一个目标,通知其他checkbox需要进行检查用于添加新checkbox的容器然后定义ConcreteSubject和ConcreteObserver处理程序,以便向页面添加新观察者,并实现更新界面。关于这些组件在示例上下文中的作用注释,请参阅下面的内容。如下是HTML代码:
如下是样例脚本:
/`
javascript
check.type = "checkbox";
// 利用Observer类扩展checkbox
extend(new Observer(), check); // 重写自定义更新行为 check.Update = function (value) {this.checked = value;
};
// 为主subject的观察者列表添加新的观察者 controlCheckbox.AddObserver(check); // 将观察者附件到容器上container.appendChild(check);
}
在本例中,我们了解了如何实现和使用Observer模式,包含目标(Subject)、观察者(Observer)、具体目标(ConcreteSubject)和具体观察者(ConcreteObserver)的概念。**9.5.1 Observer(观察者)模式和Publish/Subscribe(发布/订阅)模式的区别**通常在JavaScript里,注重Observer模式是很有用的,我们会发现它一般使用一个被称为Publish/Subscribe(发布/订阅)模式的变量来实现。虽然这些模式非常相似,但是它们之间的几点区别也是值得注意的。Observer模式要求希望接收到主题通知的观察者(或对象)必须订阅内容改变的事件,如图9-4所示。![screenshot](https://yqfile.alicdn.com/b63bd223cec5cfbca607ab63ed4c3618d1fbde9b.png)Publish/Subscribe模式使用了一个主题/事件通道,这个通道介于希望接收到通知(订阅者)的对象和激活事件的对象(发布者)之间。该事件系统允许代码定义应用程序的特定事件,这些事件可以传递自定义参数,自定义参数包含订阅者所需的值。其目的是避免订阅者和发布者之间产生依赖关系。这与Observer模式不同,因为它允许任何订阅者执行适当的事件处理程序来注册和接收发布者发出的通知。下面这个示例说明了如果有publish()、subscribe()和unsubscribe()的功能实现,是如何使用Publish/Subscribe模式的:// 非常简单的mail处理程序// 接收到的消息数量
var mailCounter = 0;
// 初始化订阅,名称是inbox/newMessage// 呈现消息预览
var subscriber1 = subscribe("inbox/newMessage", function (topic, data) {
// debug模式记录topic console.log("A new message was received: ", topic); // 使用从目标subject传递过来的data,一般呈现消息预览$(".messageSender").html(data.sender);$(".messagePreview").html(data.body);
});
// 另外一个订阅,使用同样的data数据用于不同的任务// 通过发布者更新所接收消息的数量
var subscriber2 = subscribe("inbox/newMessage", function (topic, data) {
$('.newMessageCounter').html(mailCounter++);});publish("inbox/newMessage", [{ sender: "hello@google.com", body: "Hey there! How are you doing today?"}]);// 之后可以通过unsubscribe来取消订阅
// unsubscribe( subscriber1, );
// unsubscribe( subscriber2 );这里的中心思想是促进松散耦合。通过订阅另一个对象的特定任务或活动,当任务/活动发生时获得通知,而不是单个对象直接调用其他对象的方法。**9.5.2 优点**Observer模式和Publish/Subscribe模式鼓励我们努力思考应用程序不同部分之间的关系。它们也帮助我们识别包含直接关系的层,并且可以用目标集和观察者进行替换。实际上可以用于将应用程序分解为更小、更松散耦合的块,以改进代码管理和潜在的复用。使用Observer模式背后的另一个动机是我们需要在哪里维护相关对象之间的一致性,而无需使类紧密耦合。例如,当一个对象需要能够通知其他对象,而无需在这些对象方面做假设时。在使用任何一种模式时,动态关系可以在观察者和目标之间存在。这提供了很大的灵活性,当应用程序的不同部分紧密耦合时,这可不是很容易实现的。虽然它可能不一直是所有问题的最佳解决方案,但这些模式仍是用于设计解耦性系统的最佳工具之一,应该视为所有JavaScript开发人员工具中的一个重要工具。**9.5.3 缺点**因此,这些模式的某些问题实际上源于它的主要好处。在Publish/Subscribe中,通过从订阅者中解耦发布者,它有时会很难保证应用程序的特定部分按照我们期望的运行。例如,发布者可能会假设:一个或多个订阅者在监听它们。倘若我们假设订阅者需要记录或输出一些与应用程序处理有关的错误。如果订阅者执行日志崩溃了(或出于某种原因无法正常运行),由于系统的解耦特性,发布者就不会看到这一点。这种模式的另一个缺点是:订阅者非常无视彼此的存在,并对变换发布者产生的成本视而不见。由于订阅者和发布者之间的动态关系,很难跟踪依赖更新。**9.5.4 Publish/Subscribe实现**Publish/Subscribe非常适用于JavaScript生态系统,这主要是因为在其核心,ECMAScript实现是由事件驱动的。在浏览器环境下尤其如此,因为DOM将事件是作为脚本编程的主要交互API。也就是说,在实现代码里,无论是ECMAScript还是DOM都不会提供核心对象或方法来创建自定义事件系统(或许除了DOM3 CustomEvent以外,它被绑定到DOM,因此一般是无用的)。幸运的是,流行的JavaScript库,比如Dojo、jQuery(自定义事件)和 YUI都拥有一些实用程序可以很容易实现Publish/Subscribe系统。如下是一些有关示例:
// Publish
// jQuery: $(obj).trigger("channel", [arg1, arg2, arg3]);$( el ).trigger( "/login", [{username:"test", userData:"test"}] );// Dojo: dojo.publish("channel", [arg1, arg2, arg3] );dojo.publish( "/login", [{username:"test", userData:"test"}] );// YUI: el.publish("channel", [arg1, arg2, arg3]);el.publish( "/login", {username:"test", userData:"test"} );// Subscribe// jQuery: $(obj).on( "channel", [data], fn );$( el ).on( "/login", function( event ){...} );// Dojo: dojo.subscribe( "channel", fn);var handle = dojo.subscribe( "/login", function(data){..} );// YUI: el.on("channel", handler);el.on( "/login", function( data ){...} );// Unsubscribe// jQuery: $(obj).off( "channel" );$( el ).off( "/login" );// Dojo: dojo.unsubscribe( handle );dojo.unsubscribe( handle );// YUI: el.detach("channel");el.detach( "/login" );对于那些希望使用采用纯 JavaScript(或其他库)的Publish/Subscribe模式的人来说,AmplifyJS(包含了一个整洁、与库无关的实现,它可用于任何库或工具包。值得一看的类似语言有Radio.js 、PubSubJS 、或Peter Higgins所写的Pure JS PubSub。jQuery开发人员更是有相当多的其他选择,可以选择使用众多完整实现中的一个,从Peter Higgins的jQuery插件到Ben Alman在GitHub上的优化过的Pub/Sub jQuerygist。所以我们现在能够正确了解有多少个纯JavaScript实现的Observer模式了,让我们来看一下在GitHub上发布的一个被称为pubsubz的项目中一个极简版本的Publish/Subscribe I。它展示了订阅和发布的核心概念,以及取消订阅的概念。我选择了在这个代码的基础上展示我们的示例,因为它非常接近我们所期望的JavaScript版经典Observer模式所包括的方法命名和实现方式。**9.5.4.1 Publish/Subscribe实现**
var pubsub = {};
(function (q) {var topics = {}, subUid = -1;// 发布或广播事件,包含特定的topic名称和参数(比如传递的数据)q.publish = function (topic, args) { if (!topics[topic]) { return false; } var subscribers = topics[topic], len = subscribers ? subscribers.length : 0; while (len--) { subscribers[len].func(topic, args); } return this;
};
// 通过特定的名称和回调函数订阅事件,topic/event触发时执行事件
q.subscribe = function (topic, func) {
if (!topics[topic]) { topics[topic] = []; } var token = (++subUid).toString(); topics[topic].push({ token: token, func: func }); return token;
};
//基于订阅上的标记引用,通过特定topic取消订阅
q.unsubscribe = function (token) {
for (var m in topics) { if (topics[m]) { for (var i = 0, j = topics[m].length; i < j; i++) { if (topics[m][i].token === token) { topics[m].splice(i, 1); return token; } } } } return this;
};
}(pubsub));**9.5.4.2 使用上述实现**我们现在可以使用该实现来发布和订阅感兴趣的活动,如下所示(示例9-1):示例9-1 使用上述实现
// 另一个简单的消息处理程序
// 简单的消息记录器记录所有通过订阅者接收到的主题(topic)和数据var messageLogger = function ( topics, data ) {console.log( "Logging: " + topics + ": " + data );
};
// 订阅者监听订阅的topic,一旦该topic广播一个通知,订阅者就调用回调函数var subscription = pubsub.subscribe( "inbox/newMessage", messageLogger );// 发布者负责发布程序感兴趣的topic或通知,例如:pubsub.publish( "inbox/newMessage", "hello world!" );// 或者pubsub.publish( "inbox/newMessage", ["test", "a", "b", "c"] );// 或者pubsub.publish( "inbox/newMessage", { sender: "hello@google.com", body: "Hey again!"});// 如果订阅者不想被通知了,也可以取消订阅// 一旦取消订阅,下面的代码执行后将不会记录消息,因为订阅者不再进行监听了
pubsub.publish( "inbox/newMessage", "Hello! are you still there?" );
**9.5.4.3 用户界面通知**接下来,假设我们有一个负责显示实时股票信息的Web应用程序。该应用程序有一个显示股票统计的网格和一个显示最后更新点的计数器。当数据模型改变时,应用程序需要更新网格和计数器。在这种情况下,目标(它将发布主题/通知)就是数据模型,观察者就是网格和计数器。当观察者接收到Model(模型)自身已经改变的通知时,则可以相应地更新自己。在我们的实现中,订阅者会监听newDataAvailable这个topic,以探测是否有新的股票信息。如果新通知发布到这个topic,它将触发gridUpdate向包含股票信息的网格添加一个新行。它还将更新一个last updated计数器来记录最后一次添加的数据(示例9-2)。示例9-2 用户界面通知// 在newDataAvailable topic上创建一个订阅
var subscriber = pubsub.subscribe( "newDataAvailable", gridUpdate );
// 返回稍后界面上要用到的当前本地时间
getCurrentTime = function (){
var date = new Date(), m = date.getMonth() + 1, d = date.getDate(), y = date.getFullYear(), t = date.toLocaleTimeString().toLowerCase(); return (m + "/" + d + "/" + y + " " + t);
};
// 向网格组件上添加新数据行
function addGridRow( data ) {
// ui.grid.addRow( data ); console.log( "updated grid component with:" + data );}// 更新网格上的最新更新时间
function updateCounter( data ) {
// ui.grid.updateLastChanged( getCurrentTime() );console.log( "data last updated at: " + getCurrentTime() + " with " + data);
}
// 使用传递给订阅者的数据data更新网格
gridUpdate = function( topic, data ){
if ( data !== "undefined" ) {grid.addGridRow( data ); grid.updateCounter( data );}
};
// 下面的代码描绘了数据层,一般应该使用ajax请求获取最新的数据后,告知程序有最新数据
// 发布者更新gridUpdate topic来展示新数据项
pubsub.publish( "newDataAvailable", { summary: "Apple made $5 billion", identifier: "APPL", stockPrice: 570.91});pubsub.publish( "newDataAvailable", { summary: "Microsoft made $20 million", identifier: "MSFT", stockPrice: 30.85});**9.5.4.4 使用Ben Alman的Pub/Sub实现解耦应用程序**在接下来的电影评级示例中,我们将使用Ben Alman在Publish/Subscribe模式上的jQuery实现来展示我们如何解耦一个用户界面。需要注意的是,如何提交评级才会有新用户和评级数据同时发布通知的效果。这是留给这些topic的订阅者来处理那些数据的。在我们的例子中,将新数据放入现有的数组中,然后使用Underscore库的.template()方法使用模板呈现它们。如下是HTML/模板代码(示例9-3):示例9-3 用于Pub/Sub的HTML/模板代码![screenshot](https://yqfile.alicdn.com/31575a12a8e03100dda32409c63f35b68c790ce5.png)下面是JavaScript代码(示例9-4):示例9-4 用于Pub/Sub的JavaScript代码
; (function ($) {
// 订阅new user 主题,提交评论的时候在用户列表上添加一个用户 $.subscribe("/new/user", function (e, data) {var compiledTemplate;if (data) { compiledTemplate = _.template($("#userTemplate").html()); $('#users').append(compiledTemplate(data));}
});
// 订阅new ra`
ting主题,rating主题由title和rating组成。新rating添加到已有用户的rating列表上 $.subscribe("/new/rating", function (e, data) { var compiledTemplate; if (data) { compiledTemplate = _.template($("#ratingsTemplate").html()); $("#ratings").append(compiledTemplate(data)); }});// 添加新用户处理程序$("#add").on("click", function (e) { e.preventDefault(); var strUser = $("#twitter_handle").val(), strMovie = $("#movie_seen").val(), strRating = $("#movie_rating").val(); // 通知程序,新用户有效 $.publish("/new/user", { name: strUser }); // 通知程序新rating评价有效 $.publish("/new/rating", { title: strMovie, rating: strRating }); });})(jQuery);
9.5.4.5 解耦基于Ajax的jQuery应用程序
在最后一个示例中,我们将看一下如何使用 Pub/Sub 解耦在早期开发过程中的代码,以此使我们省去一些可能繁琐的重构工作。通常在侧重Ajax技术的应用程序中,一旦我们收到了请求的响应,我们就想据此实现不只一个特定动作。我们可以简单地向成功回调中添加所有的post请求逻辑,但这种方法存在一些缺点。
由于函数/代码之间互相依赖的增加,高度耦合的应用程序有时会增加复用函数所需的工作量。这意味着,如果我们只是想一次性获取一个结果集,在回调中对post请求逻辑进行硬编码可能是行得通的,但是,当我们需要对相同的数据源(和不同的端行为)进一步地进行Ajax调用,而没有多次重写部分代码,那就不那么合适了。我们可以从一开始就使用pub/sub来节省时间,而不必遍历调用相同数据源的每一层而后再对它们进行操作。
通过使用Observer,我们还可以将不同事件降至我们所要的任何粒度级别,并轻松地根据这些事件分离应用程序范围内的通知,而使用其他模式完成这项工作的优雅度较低。
请注意在下面的样例中,当用户表示他想进行搜索查询时,是如何发出一个topic通知的,以及当请求返回并且有实际数据可用时,是如何发出另一个通知的。它让订阅者随后决定如何利用这些事件(或返回的数据)。它的好处是:如果我们愿意,我们可以有10个不同的订阅者以不同的方式使用返回的数据,但这对于Ajax层而言是无关紧要的。其唯一的责任是请求和返回数据,然后传递给任何想使用它的人。这种关注点分离能使代码的整个设计变得更加整洁。
下面是HTML/模板代码(示例9-5):
示例9-5 用于Ajax的HTML/模板
下面是JavaScript代码(示例9-6):
示例9-6 用于Ajax的JavaScript代码
; (function ($) { // 预编译模板,并使用闭包缓存它var resultTemplate = _.template($("#resultTemplate").html());
// 订阅新搜索tags主题
$.subscribe("/search/tags", function (e,tags) { $("#searchResults") .html("Searched for:" + tags + "");});
// 订阅新搜索结果主题
$.subscribe("/search/resultSet", function (e,results) { $("#searchResults").append(resultTemplate(results)); $("#searchResults").append(compiled_template(results));});
// 提交搜索请求,并在/search/tags主题上发布tags
$("#flickrSearch").submit(function (e) { e.preventDefault(); var tags = $(this).find("#query").val(); if (!tags) { return; } $.publish("/search/tags", [$.trim(tags)]);});
// 订阅发布的新tag,并且使用tag发起请求。一旦返回数据,将数据发布给应用程序的其他使用者
$.subscribe("/search/tags", function (e,tags) { $.getJSON("//api.flickr.com/services/feeds/photos_public.gne?jsoncallba ck=?", { tags: tags, tagmode: "any", format: "json" },function (data) { if (!data.items.length) { return; } $.publish("/search/resultSet", data.items); });});})(jaucry);
在应用程序设计中,Observer模式在解耦多个不同脚本方面是非常有用的,如果你还没有使用它,我建议你了解一下这里提到的其中一个预先编写的实现,并试着使用一下。这是要入门了解的一个比较简单的设计模式,但同时也是最强大的设计模式之一。
转载地址:http://uhial.baihongyu.com/