内容


深入剖析 Dojo 中的有状态对象 - dojo.Stateful

通过这种有状态对象,用户可以实现类似“数据的绑定”功能,如果用户需要一个对象的属性值始终与另一个对象的某个属性值保持一致,就能通过有状态的对象来实现,当一个有状态对象的属性值发生变化时,用户便可接收到属性值变化的消息并进而修改另一个对象的属性值。同样,如果在 dijit 中让 dojo._Widget 支持有状态对象的特性,那么对于 dijit 中的所有 widget(如:dijit.Dialog, dijit.Menu 等等),用户都能监听其属性值的变化。同样也可以将这种有状态的功能应用到 Dojo Data 中,甚至更大的范围,这样不论是 Dojo 的 data store, widget, 又或是任何继承了 Stateful 的对象都能被监听其状态。在这中基于有状态对象的应用中,用户编程的灵活性就大大高于从前了。

简介

JavaScript 里面可以创建各种各样对象,但是这些对象是无状态的,即这些对象的属性是否曾经被修改,是否正在被修改均无从得知,Dojo 针对这种需求提出了一个有状态对象的概念,并实现了支持有状态对象的控件 - dojo.Stateful,本文主要来探讨一下该控件的功能,以及其实现的细节。

使用示例

基本用法:

清单 1. stateful 对象基本用法
 var s = new dojo.Stateful({ 
 foo: 3 
 }); 
 console.log(s.get("foo"));//3 
 var watching = s.watch("foo", function(name, oldValue, value){ 
 console.log(name); //"foo"
 console.log(oldValue); //3 
 console.log(value); //4 
 console.log(s.get("foo"));//4 
 }); 
 s.set("foo", 4);// 触发
 console.log(s.get("foo"));//4 
 watching.unwatch();// 停止监听
 s.set("foo", 5);

可以看到,Dojo 的有状态对象其用法是用 dojo.Stateful 控件将 JavaScript 对象包装起来的,从代码上可以看出,这里 s 就是一个有状态对象,有一个属性叫 foo。我们通过“get”方法获取该对象的属性值,“set”方法修改属性值。第一个“console.log”的输出结果为“3”。

接下来我们讨论一下“watch”方法,这也是有状态对象的关键所在。我们之所以能察觉到有状态对象 s 的属性变化,主要是因为“watch”方法的作用。

这里我们可以看到,有状态对象有一个“watch”方法,第一个参数是要监测的属性;第二个参数是一个 callback 函数,即当有状态对象的属性被修改时会触发,它有三个参数:属性名,原始值,新值。我们可以参考上面的例子,注释“触发”处,当程序调用“set”方法时,会触发 callback 函数,可以看到“watch”方法的回调函数里面,属性名为“foo”,原始值为 3,新值为 4,同样,你也可以直接访问 s,调用其“get”方法获取当前属性值。

最后,还要注意一个方法:“unwatch”,该方法用于解除监听的绑定。此时的 s.set("foo", 5) 便不会触发回调函数。

Set 方法的使用:

清单 2. set 方法
 var s = new dojo.Stateful(); 
 s.set({ 
 foo:3, 
 bar: 5 
 }); 
 console.log(s.get("foo"));//3 
 console.log(s.get("bar");//5

上述代码提供了另外一种有状态对象的初始化方法,直接用“set”方法初始化,可以看到(console.log 处),其“foo”和“bar”值分别为 3 和 5。

除了初始化,它也可用于属性的添加和属性值的修改:

清单 3. set 方法初始化
 var s = new dojo.Stateful(); 
 s.set({ 
 foo:3, 
 bar: 5 
 }); 
 console.log(s.get("foo"));//3 
 console.log(s.get("bar"));//5 

 s.set({ 
 foo:  10, 
 foo1: 13, 
 bar1: 15 
 }); 
 console.log(s.get("foo"));//10 
 console.log(s.get("bar"));//5 
 console.log(s.get("foo1"));//13 
 console.log(s.get("bar1"));//15

可以看到(注释“//10”处),其“foo”的值变成 10。同样,新增加了“foo1”和“bar1”两个属性,其值分别为 13 和 15。

使用方式进阶

Stateful 对象

通过前面的代码,我们了解了 Dojo 有状态对象的使用方式,其最关键的突破就是“watch”方法,它使得动态监听对象属性值的变化成为可能。我们也可以看到,Dojo 有状态对象的对属性的访问,添加和修改等等操作都是通过“get”和“set”方法完成的,这也是公认比较好的一种属性访问的标准方式。但是,这些属性及其值具体存放在哪里呢?我们看一下如下代码:

清单 4. Stateful 对象
 var s = new dojo.Stateful(); 
 s.set({ 
 foo:3, 
 bar: 5 
 }); 

 s.set({ 
 foo:  10, 
 foo1: 13, 
 bar1: 15 
 }); 

 console.log(s)

查看 firebug,可以看到“s”的属性列表如图 1:

图 1. “S”的属性
“S”的属性
“S”的属性

可以看到,其实属性值全部存放在 Stateful 对象本身上,还有一些内置的方法,包括“get”,“set”,“postscript”等等。

属性的直接访问

所以,我们也可以通过如下方式访问 Dojo 有状态对象的属性:

清单 5. Stateful 对象属性的 set 访问
 var s = new dojo.Stateful(); 
 s.set({ 
 foo:3, 
 bar: 5 
 }); 

 s.set({ 
 foo:  10, 
 foo1: 13, 
 bar1: 15 
 }); 

 console.log(s.foo);//10 
 console.log(s.bar);//5 
 console.log(s.foo1);//13 
 console.log(s.bar1);//15

直接访问有状态对象的属性这种方式我们是不推荐的,不仅仅因为这种访问方式不符合标准,它还会造成有状态对象的失效,我们来看看如下代码:

清单 6. Stateful 对象属性的直接访问方式
 var s = new dojo.Stateful(); 
 s.set({ 
 foo:3, 
 bar: 5 
 }); 
 console.log(s.get("foo"));//3 
 console.log(s.foo);//3 

 var watching = s.watch("foo", function(name, oldValue, value){ 
 console.log("accessed");// 没有访问 
 console.log(name); // 没有访问 
 console.log(oldValue); // 没有访问 
 console.log(value); // 没有访问 
 }); 
 s.foo = 13; 
 console.log(s.get("foo"));//13 
 console.log(s.foo);//13

可以看到(第一个 console.log 处),通过“get”访问,或直接属性访问都可以拿到原始值“3”。然后,我们监听“foo”属性。当我们通过属性直接访问的方式修改“foo”属性时(s.foo = 13),callback 回调函数并没有被触发(“没有访问”),所以,如果不按照标准方式访问 Dojo 有状态对象的属性,那么它的有状态特性就无从谈起了。

同一属性绑定多个回调函数

先看看如下代码:

清单 7. 绑定多个回调函数
 var s = new dojo.Stateful({ 
 foo: 3 
 }); 
 var watching = s.watch("foo", function(name, oldValue, value){ 
   console.log("first watch callback"); 
   }); 
 var watching1 = s.watch("foo", function(name, oldValue, value){ 
 console.log("second watch callback"); 
    }); 
 s.set("foo", 4);// 触发
 watching.unwatch();// 停止监听
 watching1.unwatch();// 停止监听

我们对属性“foo”同时绑定两个 callback 函数(watching 和 watching1),当调用“set”方法时,两个回调函数同时被调用,并且绑定在先,则调用在先。

核心实现机制

使用方式说完了,接下来我们要谈一谈它的具体实现了,看看 Dojo 的有状态对象究竟是如何实现它的有状态特性的。

Get 方法的实现

Get 方法的实现代码如下:

清单 8 Get 方法的实现
 get: function(/*String*/name){ 
 return this[name]; 
 },

可以看到,非常简单的一句话,通过这里我们可以看到,其实它就是访问自身的属性,这也解释了为什么有状态对象(“s”)的属性值全部存放在有状态对象本身上。

Set 方法的实现

Set 方法的实现代码如下:

清单 9. Set 方法的实现
 set: function(/* 字符串 */name, /* 对象 */value){ 
 if(typeof name === "object"){ 
 for(var x in name){ 
 this.set(x, name[x]); 
 } 
 return this; 
 } 
 var oldValue = this[name]; 
 this[name] = value; 
 if(this._watchCallbacks){ 
 this._watchCallbacks(name, oldValue, value); 
 } 
 return this; 
 },

可以看到,第一个“if”语句内部解释了为什么可以向“set”方法里传对象:它实际上遍历传入对象所有属性,然后分别调用“set”方法。

在更新属性值以后,调用之前通过“watch”绑定好的 callback 回调函数,从这里可以看出,“watch”方法应该主要是操作“_watchCallbacks”这个私有变量了。这里也可以解释为什么不按照标准方式(“set”方法,非属性直接访问)访问 Dojo 有状态对象的属性,它的有状态特性就无从谈起。

Watch 方法的实现

Watch 方法的实现代码如下:

清单 10. watch 方法的实现
 watch: function(/* 字符串 ?*/name, /* 函数 */callback){ 
 var callbacks = this._watchCallbacks; 
 if(!callbacks){ 
 var self = this; 
 callbacks = this._watchCallbacks = function(name, oldValue, value, 
 ignoreCatchall){ 
 var notify = function(propertyCallbacks){ 
				 for(var i = 0, l = propertyCallbacks && 
				 propertyCallbacks.length; i < l; i++){ 
				 try{ 
				 propertyCallbacks[i].call(self, name, 
				 oldValue, value); 
				 }catch(e){ 
				 console.error(e); 
				 } 
				 } 
				 }; 
				 notify(callbacks[name]); 
 if(!ignoreCatchall){ 
 notify(callbacks["*"]); // the catch-all 
 } 
 }; 
 conversion 
 } 
 if(!callback && typeof name === "function"){ 
 callback = name; 
 name = "*"; 
 } 
 var propertyCallbacks = callbacks[name]; 
				 if(typeof propertyCallbacks !== "object"){ 
				 propertyCallbacks = callbacks[name] = []; 
				 } 
				 propertyCallbacks.push(callback); 
 return { 
 unwatch: function(){ 
 propertyCallbacks.splice(dojo.indexOf(propertyCallbacks, 
 callback),1); 
 } 
 }; 
 },

我们先看下面的“propertyCallbacks”变量处,“name”是属性名,callbacks(也是 this. _watchCallbacks)的“name”属性值是一个数组,里面存放了所有通过“watch”方法绑定到该属性的 callback 回调函数,这也解释了为什么可以在同一个属性上绑定多个回调函数,它们之间的关系并不是前一个被后一个覆盖,而是一个一个叠加上去的。

再来分析一下 callback 回调函数是如何被调用的。之前我们在“set”方法里看到如下代码:

清单 11. callback 方法的调用
 if(this._watchCallbacks){ 
 this._watchCallbacks(name, oldValue, value); 
 }

从这里我们可以知道,它实际上是执行了如上“watch”源代码里的“notify”函数变量那一段。从 “notify(callbacks[name]);”这里切入,我们知道,“callbacks[name]”这里是一个数组,里面存放了所有通过“watch”方法绑定到该属性的 callback 回调函数。所以“notify”的实现里面就是一个 for 循环,调用所有的绑定到该属性回调函数:“propertyCallbacks[i].call(self, name, oldValue, value);”。

Dojo 有状态对象的发展趋势

Dojo1.5 的有状态对象现在基本只提供了“get”和“set”方法,其实 Dojo 有状态对象可以有更为广泛的用途。如果 Dojo 的后期版本中让 dojo._Widget 继承自 dojo.Stateful,使得 dijit 的每个 widget 也具有“有状态”的特性,即:dijit 中的所有 widget 我们都能监听器状态的变化,就像我们监听基本对象的属性变化一样。那将是 dijit 的一次革命性的变化,有状态的 widget 也同样会基于 Stateful 对象而衍生出很多很有用的新功能。

其实,实现有状态对象最根本的动机是实现“数据的绑定”功能,比如说:我们需要一个对象的属性的值始终与另一个对象的某个属性的值保持一致,又或是当我们修改某个对象属性的同时,界面上会根据此对象属性的变化作出相应的变化,实现重新绘制(render)的效果;这些需求我们都能通过有状态的对象来实现,我们所要做的就是对这些对象的属性实现一个“watch”方法,并在方法中做出相应的实现即可。

结束语

这篇文章介绍了 Dojo 有状态对象的基本用法和一些进阶用法,分析了标准使用方式和一些特殊使用方式的优缺点。深入剖析了 Dojo 有状态对象的核心实现机制,解释了 Dojo 是如何实现有状态对象的。最后,也分析了一下 Dojo 有状态对象的未来。


相关主题

  • Dojo 校园文档主页:http://docs.dojocampus.org/Dojo 中控件的比较完全的 API 文档主页,包括 dojo,dijit,dojox 等等。
  • Dojo 官方文档主页:http://dojotoolkit.org/documentation/Dojo 官方的很多支持 Ajax 应用程序开发的组件的文档。
  • Dojo 技术专题”(developerWorks,2009 年 12 月):伴随 Web 2.0,Ajax 和 RIA 的热潮,各种 JavaScript 开发工具包如雨后春笋般蓬勃发展,Dojo 正是这些工具包中的佼佼者。Dojo 为富互联网应用程序(RIA)的开发提供了完整的端到端的解决方案,包括核心的 JavaScript 库,简单易用的小部件(Widget)系统和一个测试框架,此外,Dojo 的开源开发社区也在不停地为它提供新的功能。本专题汇集了与 Dojo 相关的技术资源。
  • Dojo Query 详解”(developerWorks,2010 年 9 月):在 Web 应用程序开发中,JavaScript 的应用越来越普遍,越来越复杂,一个 Web 页面中往往有成百上千个 HTML 元素,准确、高效地选择所需的元素并对其进行操作,不仅可以在程序开发阶段节省编码时间,降低程序出错的概率,在运行还能提高程序运行效率,提供更好的用户体验。Dojo 提供了功能强大的 Query 函数库,使用一个高效的查询引擎,能够根据元素 id,名称,CSS,属性及其组合对页面的元素进行查询,并且能够对返回结果进行非常方便的处理。本文详细介绍 dojo.query 的各种查询方式,以及常用对结果的处理方式。恰当使用 dojo.query 能够大大提高程序开发的效率,减少代码量,提高编码质量。
  • 使用 Dojo 开发菜单应用”(developerWorks,2010 年 10 月):菜单应用是 Web 页面的点睛之笔。Dojo 提供的菜单库,除实现了菜单的基本功能外,还加入对弹出式菜单、图标效果、键盘响应等功能的支持,方便了开发人员的菜单开发过程。
  • 使用 Dojo 开发基于 iPad 的 Web 应用程序”(developerWorks,2011 年 2 月):我们在为 iPad 设计 Web 程序时,经常会遇到比较复杂的需求。Dojo 在 1.5 版本中加入了 dojox.mobile 包,开发者可以更方便地构建移动平台的 Web 程序。但是,dojox.mobile 包大多是提供类似手机本地应用程序的外观和动画效果,因此我们还是会频繁用到 dojo 提供的其它控件,这时可能就会出现一些问题。本文从实际项目开发中举了几个这样的例子来说明如何进行改进,使得 dojo 控件在 iPad 中表现得更加自然。
  • 拖拽:从 Dojo 到 HTML5”(developerWorks,2011 年 2 月):拖拽是 Web 2.0 应用中最流行的技术之一。本文将介绍如何在网络应用程序中使用 dojo 和 HTML5 这两种技术的拖拽功能。并将通过示例详细介绍 HTML5 的拖拽功能。
  • developerWorks Web development 专区:通过专门关于 Web 技术的文章和教程,扩展您在网站开发方面的技能。
  • developerWorks Ajax 资源中心:这是有关 Ajax 编程模型信息的一站式中心,包括很多文档、教程、论坛、blog、wiki 和新闻。任何 Ajax 的新信息都能在这里找到。
  • developerWorks Web 2.0 资源中心,这是有关 Web 2.0 相关信息的一站式中心,包括大量 Web 2.0 技术文章、教程、下载和相关技术资源。您还可以通过 Web 2.0 新手入门 栏目,迅速了解 Web 2.0 的相关概念。
  • 查看 HTML5 专题,了解更多和 HTML5 相关的知识和动向。
static.content.url=http://www.ibm.com/developerworks/js/artrating/
SITE_ID=10
Zone=Web development
ArticleID=732546
ArticleTitle=深入剖析 Dojo 中的有状态对象 - dojo.Stateful
publish-date=07132011