B3.4 Ajax的事件处理以及函数作用域
Ajax的事件处理跟大多数GUI工具包语言一样,像我们在第四章看到的一样,有现成的鼠标和键盘事件。我们的例子用了onclick句柄,当鼠标在一个可见的元素上单击的时候会触发这个事件。DHTML的事件处理不在本书的讨论之列,但是我们看看其中的一些部分。
事件处理可以以HTML标记中声明,例如:
<div id='myDiv' οnclick='alert:alert(this.id)'></div>
或者通过程序,例如:
function clickHandler(){ alert(this.id); }
myDiv.οnclick=clickHandler;
在这里需要注意一下:我们给函数这个对象传递了一个引用,也就是说在clickHandler后面没有()。再HTML中定义函数的时候,我们可以定义一个匿名的函数,例如:
myDiv.οnclick=function(){ alert(this.id); }
注意,在这两种情况下,函数都没有指定的参数,也没有可以通过鼠标点击传进去的参数,但是,当我们在DOM元素上点击的时候,这个函数会被作为事件对象的一个参数来调用,这个元素就成了范围对象。知道这个可以减少很多麻烦,尤其是在编写基于对象的代码的时候。但是常常让人不解的是,DOM节点总是作为一个上下文来传递,即使这个对象是另一个对象的原型所定义的。
在下面的例子中,我们定义了一个具有对可见GUI元素事件处理功能的简单的对象,我们可以把这个对象看作MVC中的M,也就是模型,事件处理则是控制器,那么DOM元素就是View。
function myObj(id,div){
this.id=id;
this.div=div;
this.div.οnclick=this.clickHandler;
}
构造函数有一个内部用得ID和一个指定了onclick句柄的DOM元素,我们可以这样定义事件处理:
myObj.prototype.clickHandler=function(event){
alert(this.id);
}
所以,当我们在GUI元素上点击的时候,就应该会弹出对话框来告诉我们它的ID,对吗?实际上,不是这样的,因为myObj.clickHandler函数会被浏览器借用(就像我们前面讲的小狗从树对象借了一个方法一样),而且会被元素的上下文引用,而不是Model对象。因为元素正好有一个内在的id属性,结果还是会显示一个值,而且,根据你的命名方式,甚至有可能是跟Model对象ID相同,这里先不解释。
如果我们想让事件处理其引用我们指定的Model对象,我们就需要用另一种方法来把引用传给这个对象。通常来说有两种方法可以实现,依我看来,有一种比另一种好一点,但是这些年来我一直都在用另一种。我们这本书的一个目的就是要明确我们通常所用的模式的名字,所以我在这里两种都会说明。
通过名字引用Model
在这个方案中,我们给Model对象的每一个实例都指定一个全局的唯一的ID,并且将这些通过ID引用的对象都存放在一个全局数组中,给定一个DOM元素的引用,我们可以通过使用部分ID作为键来完成在数组中对这个Model对象的引用。下图B.1展示了这种策略。在这种方法中,首先要为每一个元素生成一个唯一的ID,但是这个生成过程可以自动地完成。我们可以把这个数组的长度作为键的一部分,例如,如果我们在web服务器上生成代码的时候的一个数据库键。
据一个简单的例子,我们创建一个myObj的对象,myObj有一个可以点击的标题栏,点击的时候可以触发myObj.foo()这个函数。
全局数组:
var MyObjects=new Array();
构造函数,同数组榜定到一起:
function myObj(id){
this.uid=id;
MyObjects[this.uid]=this;
...
this.render();
}
接下来是myObj对象的一个方法,他会完成一些令人激动的事情。我们想在标题栏被点击的时候引用它:
myObj.prototype.foo=function(){
alert('foooo!!! '+this.uid);
}
然后是render()方法,这个方法将创建几个DOM结点:
myObj.prototype.render=function(){
...
this.body=document.createElement("div");
this.body.id=this.uid+"_body";
...
this.titleBar=document.createElement("div");
this.titleBar.id=this.uid+"_titleBar";
this.titleBar.οnclick=fooEventHandler;
...
}
当我们在Model对象的view建立任何DOM结点的时候,我们就给他们指定一个包括Model对象ID的ID值。
注意我们引用了一个fooEventHandler()函数并且把它设为我们标题栏DOM元素的onclick属性:
function fooEventHandler(event){
var modelObj=getMyObj(this.id);
if (modelObj){ modelObj.foo(); }
}
}
事件处理器函数需要找到myObj的实例以便可以引用它的foo()方法。我们来写一个寻找方法:
function getMyObj(id){
var key=id.split("_")[0];
return MyObjects[key];
}
他引用了DOM结点并且可以使用它的id属性来从可以便利Model对象的全局数组里取得一个键。这就是了,通过名字引用Model这种方式我已经用了好几年,并且很好用,但是还有一种更简洁的方法,不需要通过长度来引用DOM树。
给DOM结点增加一个Model
在这第二种DOM事件处理中,所有的事情都是通过对象引用来完成,而不是字符串,也不需要一个全局的数组变量。这就是本书中一直使用的方法。图B.2展示了这种方法。
这种方法简化了事件处理的工作。Model对象的构造函数不需要手动地初始化ID,foo()方法也像从前一样定义。当创建DOM结点时,我们利用JavaScript的动态性,给对象添加属性,并且用以下方法将Model对象直接控制到DOM结点:
myObj.prototype.createView=function(){
...
this.body=document.createElement("div");
this.body.modelObj=this;
...
this.titleBar=document.createElement("div");
this.titleBar.modelObj=this;
this.titleBar.οnclick=fooEventHandler;
...
}
当我们编写事件处理器的时候,我们可以直接取得Model的引用:
function fooEventHandler(event){
var modelObj=this.modelObj;
if (modelObj){ modelObj.foo(); }
}
}
没有发现者,没有全局查找,就是这么简单。
在这里,需要注意一点,在使用这种模式时,我们在DOM和非DOM变量之间创建了一个循环引用,在现有的浏览器中,可能有的浏览器对垃圾回收做得并不好。如果正确地运用这种模式,可以避免内存溢出,我建议你在实现给DOM结点增加Model模式之前先学习第七章。
理解JavaScript函数是怎么定义上下文环境可以帮助我们开发针对浏览器事件模型的大型的,可重用的解决方案。在不同环境中切换函数开始可能有一些难理解,但是理解它可以让我们更好的使用它。
我们关于JavaScript函数需要知道的最后一件事情就是这种语言创建closure的能力。Java和C#缺少closures的观念,虽然Groovy和Boo等这些Java和.NET脚本语言支持,而且C#2.0也会支持。现在就让我们看看他们到底是什么,我们应该如何使用。