19.2 2级DOM中的高级事件处理
迄今为止,我们在本章中看到的事件处理方法都是0级DOM(所有启用javascript的浏览器都支持的实际标准API)的一部分。2级DOM标准定义了高级事件处理API,它与0级API有很大不同(而且强大得多)。虽然2级标准没有把现有的API集成到DOM标准中,但舍弃0级API不会有任何危险。对于基本的事件处理任务应该继续用简单API。
Mozilla和Netscape 6支持2级DOM的Event模型,但Internet Explorer 6不支持它。
19.2.1 事件传播
在0级DOM事件模型中,浏览器把事件分派给发生事件的文档元素。如果那个对象具有适合的事件处理程序,就运行这个处理程序。除此之外,不用执行其它的操作。在2级DOM中,情况要复杂得多。在这种高级事件模型中,当事件发生在Document节点(即事件目标)时,目标的事件处理程序就被角发。此外,目标的每个祖先节点也有机会处理那个事件。事件传播分三个阶段进行。第一,在捕捉(capturing)阶段,事件从Document对象沿着文档树向下传播给目标节点。如果目标的任何祖先(不是目标自身)专门注岫了捕捉事件的处理程序,那么在事件传播过程中,就会运行这些处理程序(不久我们将学习如何注册常规的事件处理程序和捕捉事件处理程序)。
事件传播的下一个阶段发生在目标节点自身,直接注册在目标上的适合的事件处理程序将运行。这与0级事件模型提供的事件处理方法相似。
事件传播的第三个阶段是起泡(bubbling)阶段,在这个阶段,事件将从目标元素向上传播回或起泡回Document对象的文档层次。虽然所有事件都受事件传播的捕捉阶段的支配,但并非所有类型的事件都起泡。例如,把提交事件从<form>元素向上传播到控制它的文档元素是毫无意义的。另一方面,文档中的所有元素都对普通事件(如mousedown)感兴趣,所以它们可以沿着文档层次起泡,角发目标元素的祖先的适当的事件处理程序。一般说来,原始输入事件起泡,而高级语义事件不起泡(参阅本章后面的表19-3,它完整地列出了哪些事件起泡,哪些不起泡)。
在事件传播过程中,任何事件处理程序都可以调用表示那个事件的Event对象的stopPropagation()方法,停止事件的进一步传播。在本章后面的小节中,我们将看到更多有关Event对象和stopPropagation()方法的内容。
有些事件会引发浏览哭执行相关的默认动作。例如,在点击<a>标记进,浏览器的默认动作是进行超链接。这样的默认动作只在事件传播的三个阶段都完成之后才会执行,事件传播过程中调用的任何处理程序都能通过调用Event对象的preventDefault()方法阻止默认动作发生。
虽然这种事件传播看起来很复杂,但它提供了重要的中心化事件处理化码的方法。2级DOM暴露所有的文档元素,允许事件(如mouseover事件)在任何元素上发生。
这意味着注册事件处理程序的地方比老式的0级事件模型多得多。假定想在用户把鼠标移到文档中的<p>元素上时角发一个事件处理程序,那么不必为每个<p>标记都注册一个onmouseover事件处理程序,只在Document对象上注册一个onmouseover,然后在事件传播的捕捉或起泡阶段处理这些事件即可。
关于事件传播,有一个重要的细节。在0级模型中,只能为特定的对象的特定类型的事件注册一个事件处理程序。但在2级模型中,就可以为特定对象的特定类型事件注册任意多个处理函数。这同样适用于事件目标的祖先,它们的处理函数将在事件传播的捕捉阶段或起泡阶段调用。
19.2.2事件处理程序的注册
在0级API中,通过在HTML中设置性质或在javascript代码中设置对象的属性,来注册事件处理程序。在2级事件模型中,可以调用对象的addEventListener()方法为特定元素注册事件处理程序(DOM标准在它的API中使用术语“监听者”,但在我们的讨论中将继续使用同义词“处理程序”)。这个方法有三个参数。第一个参数是要注册处理程序的事件类型名。事件类型应该是含有小写HTML处理程序性质名的字符串,没有前缀“on”。因此,如果用HTML性质onmousedown,或在0级模型中用onmousedown属性,那么在2级事件模型中就应该使用字符串“mousedown”。
第二个参数是处理函数(或监听者),在指定类型的事件发生时调用该函数。在调用这个函数时,传递给它的唯一参数是Event对象。这个对象存放了有关事件(如鼠标按钮被按下)的细节,并定义了stopPropagation()这样的方法。此后我们将学习更多有关Event接口和它的子接口的内容。
addEventListener()的最后一个参数是一个布尔值。如果值为true,则指定的事件处理程序将在事件传播的捕捉阶段用于捕捉事件。如果该参数值为false,则事件处理程序就是常规的,当事件直接发生在对象上,或发生在元素的子女上,又向上起泡到该元素时,该处理程序将被触发。
例如,可以用下列addEventListener()方法,为<form>元素的提交事事注册一个处理程序:
document.myform.addEventListener("submit", function(e) {validate(e.target);}, false);
如果想捕捉<div>元素中发生的所有mousedown事件,可以使用如下addEventListener()方法:
var mydiv = document.getElementById("mydiv");
mydiv.addEventListener("mousedown", handleMouseDown, true);
注意,这些例子假定你在javascript代码的某些地方定义了名为validate()和handleMouseDown()的方法。
用addEventListener()方法注册的事件处理程序在定义它们的作用域中执行,不是由定义为HTML性质的事件处理程序使用的作用域链调用(参阅19.1.6小节)。
因为在2级模型中通过调用方法注册事件处理程序,而不是设置HTML性质或javascript属性来注册事件处理程序,所以可以给一个指定的对象的指定类型的事件注册多个事件入理程序。如果多次调用addEventListener()方法,给同一个对象同一类型的事件注册了多个处理函数,那么在该类型 的事件在那个对象上发生(或起泡到,或被捕捉到)时,被注册的所有函数都将被调用。DOM标准不确定调用一个对象的事件处理函数的顺序,所以不应该认为它们以注册的顺序调用。另外还要注意,如果在同一个元素上多次注册了同一个处理函数,那么第一次注册后的所有注册都将被忽略。
为什么想让同一个对象上的同一种事件具有多个处理函数呢?因为它对模快化软件非常有用。例如,假定你编写了一个可重用的javascript代码模块,想使用一个mouseover事件在浏览哭的状态栏中显示图像的信息(或图像表示的链接)。在0级API中,必须把两个模块合并成一个,以便它们能共享Image对象的onmouseover属性。但在2级API中,每个模块都可以注册到它需要的事件处理程序,而不需要了解或干涉其它模块。
与addEventListener()方法配对的是removeEventListener()方法,它的三个参数与前者相同,不过是从对象中删除事件处理函数,而不是添加它。通常,临时注册一个事件处理函数,用完后迅速删除它比较有效。例如,在得到一个mousedown事件时,可以为mousemove事件和mouseup事件注册临时捕捉事件处理程序,以便查看用户是否拖动了鼠标。在mouseup事件发生时,注销这些处理程序。在这种情况下,事件处理程序删除代码如下所示:
document.removeEventListener("mousemove", handleMouseMove, true);
document.removeEventListener("mouseup", handleMouseUp, true);
addEventListener()方法和removeEventListener()方法都由EventTarget接口定义。在支持2级DOM Event API的Web浏览器中,所有Document节点都实现了这个接口。要更详细地了解事件处理程序的注册和注销方法,请查阅DOM 参考手册部分的EventTarget接口。
有关事件处理程序注册,最后需要注意的一点是,在2级DOM中,事件处理程序不仅限于文档元素,还可以给Text节点注册处理程序。但在实践中,给包容元素注册处理程序,允许Text节点的事件起泡,在容器层处理这些事件会比较简单。
19.2.3 addEventListener()方支和this关键字
在原始的0级事件模型中,当函数被注册为一个文档元素的事件处理程序时,它就成为了那个文档元素的方法。当调用这个事件处理程序时,将把它作为元素的方法调用。在函数中,关键字this引用的就是发生事件的元素。
在Mozilla和Netscape 6中,当用addEventListener()方法注册了一个事件处理程序时,将以同样的方式处理它,即当浏览器调用那个函数时,便将它作为注册该函数的文档元素的方法调用。但要注意,这是与实现有关的行为,DOM标准没有要求这样执行。因此在使用2级事件模型时,不应该依赖事件处理函数中的关键字this,而应该用传递给处理函数的Event对象的属性currentTarget。在本章后面讨论Event对象时,我们会看到,curentTarget属性引用一个对象,访对象注册了当前的事件处理程序,而且是以可移植的方式注册的。
19.2.4 把对象注册为事件处理程序
可以用addEventListener()方法注册事件处理函数。如前面一节讨论的,这些函数是否作为注册它们的对象的方法调用,与实现有关。对于面向对象的程序设计方法来说,你更愿意把事件处理程序定义为定制对象的方法,然后作为那个对象的方法调用它们。对于Java程序设计者,DOM标准完全允许这样做,即事件处理程序是实现了EventListener接口和handleEvent()方法的对象。在java中,当注册一个事件处理程序时,传递经addEventListener()方法的是一个对象,而不是一个函数。为了简单起见,DOM API的javascript规约不要求实现EventListener接口,而只允许给addEventListener()方法直接传递函数引用。
在编写面向对象的javascript程序时,如果想用对象作为事件处理程序,那么可以使用如下函数来注册它们:
function registerObjectEventHandler(elenment,eventtype,listener,captures)
{
element.addEventListener(eventtype,function(event){listener.handleEvent(event);},captures);
}
用这个函数可以把任何对象注岫为事件处理程序,只要它定义了handleEvent()方法。该方法作为监听程序对象的方法而调用,关键字this引用的是监听程序对象,而不是生成事件的文档元素。这个函数可以运行,因为它使用了嵌套函数直接量,可以捕捉并记住它的作用域链中的监听程序对象(如果你忘了这一点,可以复习第11.4小节)。
虽然不是DOM标准的一部分,但Mozilla 0.9.1和Netscape 6.1(但不是Netscape 6.0或6.01)都允许直接把定义了handleEvent()方法的事件监听程序对象传递给addEventListener()方法而不是函数引用。对于这些浏览器来说,不需要我们刚才定义的特殊注册函数。
19.2.5 事件模块和事件类型
前面提到过,2级DOM标准是模块化的,所以一个实现可以只支持它的某些部分,省略对其他部分的支持。Event API阿姨是这样一个模块。可以用下面的代码测试浏览器是否支持该模块:
document.implementation.hasFeature("Events","2.0")
但Event模块只有用于基本事件处理基础结构的API。对特定类型的事件的支持则交给子模块。每个子模块提供对某类相关类型的事件的支持,并定义一个Event类型,该类型是传递给其他类型的事件处理程序。例如,MouseEvents子模块提供对mousedown、mouseup、click和相关事件类型的支持。它还定义了MouseEvent接口。实现该接口的对象将传递给访模块支持的其它事件类型的处理函数。
表19-2列出了所有事件模块,以及它定义的事件接口和它支持的事件类型。注意,2级DOM没有标准化任何类型的键盘事件,所以表中没有列出键盘事件模块。预计3级DOM标准会支持这种事件类型。
表19-2:事件模块,接口和类型
模块名:HTMLEvents
事件接口:Event
事件类型:abort,blur,change,error,focus,load,reset,resize,scroll,select,submit,unload
模块名:MouseEvents
事件接口:MouseEvent
事件类型:click,mousedown,mousemove,mouseout,mouseover,mouseup
模块名:UIEvents
事件接口:UIEvent
事件类型:DOMActivate,DOMFocusIn,DOMFocusOut
模块名:MutationEvents
事件接口:MutationEvent
事件类型:DOMAttrModified,DOMCharacterDataModified,DOMNodeInserted,DOMNodeInsertedIntoDocument,DOMNodeRemoved,DOMNodeRemovedFromDocument,DOMSubtreeModified
从表19-2中可以看到,HTMLEvents和MouseEvents模块定义的事件类型与0级事件模块中的事件类型相似。UIEvents模块定义的事件类型与HTML表单元素支持的聚焦、取消焦点和点击事件相似,不过对它们进行了推广,任何能接收焦点或以其他方式激活的文档元素都可以生成这一事件。MutationEvents模块定义的事件是在文档改变(这种改变是突变)时生成的。这些都是特殊的事件类型,并不常用。
前面提到过,在事件发生时,将传递给它的处理程序一个对象,访对象实现了与该事件类型相关的Event接口。这个对象的属性提供了事件的细节,而不是事件以模块组织它们。对于每种事件类型来说,该表列出了传递给它的处理程序的事件对象,这种事件类型是否在事件传播过程中向文档上层起泡(B列)以及这种事件是否具有能用preventDefault()方法取消的默认动作(C列)。对于HTMLEvents模块中的事件,该表的第五列列出了哪些HTML元素能生成该事件。对于其他事件类型,第五列给了事件对象的哪些属性包含有意义的值(下一节介绍这些属性)。注意,这一列中列出的属性不包括基本的Event接口定义的属性,它们对所有事件类型都包含有意义的值。
比较表19-3和表19-1很有用,表19-1列出的是HTML4定义的0级事件处理程序。两件模块支持的事件类型大部分相同(除了UIEvents模块和MutationEvents模块)。2级DOM标准添加了对abort,error,resize和scroll事件类型的支持,它们是由HTML 4标准化的事件类型(不久我们会看到,传递给click事件处理程序的对象的detail属性声明了已经发生的连续点击次数)。
19.2.6 Event接口和Event的深入研究
当事件发生时,2级DOM API提供了事件的额外信息(如事件是何时何地发生的),作为传递给事件处理程序的对象的属性。每个事件模块都有一个相关的事件接口,该接口声明了该种事件类型的详细信息。表19-2(出现在本章前面的小节中)列出了四种不同的事件模块和四种不同的事件接口。
这四个接口彼此相关,构成了一个层次。Event接口是这个层次的根,所有事件对象实现了这个最基本的事件接口。UIEvent是Event接口的子接口,实现了UIEvent接口的事件对象也实现了Event接口的所有方法和属性。MouseEvent接口是UIEvent接口的子接口,这意味着,传递给click事件的事件处理程序的事件对象实现了MouseEvent接口,UIEvent接口和Event接口定义的所有方法和属性。最后,MutationEvent接口是Event接口的子接口。
接下来的几节介绍了各种事件接口,并且强调了它们最重要的属性和方法。在本书的DOM参考手册部分可以找到每个接口的完整细节。
19.2.6.1 Event
HTMLEvent模块定义的事件类型使用Event接口。其它事件类型都使用该接口的子接口,这意味着所有事件对象都实现了Event接口,并提供了适用于所有事件类型的详细信息。Event接口定义了如下属性(注意,这些属性和所有Event子接口的属性的是只读的);
type:发生的事件的类型。该属性的值是事件类型名,与注册事件处理程序时使用的字符串值相同(例如,“click"或“mouseover“)。
target:发生事件的节点,可能与currentTarget不同。
currentTarget:发生当前正在处理的事件的节点(如当前正在运行事件处理程序的节点)。如果在传播过程的捕捉阶段或起泡阶段处理事件,这个属性的值就与target属性的值不同。前面讨论过,在事件处理函数中,应该用这个属性而不是this关键字。
eventPhase:一个数字,指定了当前所处的事件传播过程的阶段。它的值是常量,可能值包括Event.CPATURING_PHASE,Event.AT_TARGET或Event.BUBBLING_PHASE。
timeStamp:一个Date对象,声明了事件何时发生。
bubbles:一个布尔值,声明该事件(和这种类型的事件)是否在文档树中起泡。
cancelable:一个布尔值,声明访事件是否具有能用preventDefault()方法取消默认动作。
除了这七个属性外,Event接口还定义了两个方法,即stopPropagation()和preventDefault(),所有事件对象都实现了它们。调用stopPropagation()方法可以阻止事件从当前丄在处理它的节点传播。任何事件处理程序都可以调用preventDefault()方法阻止浏览器执行与事件相关的默认动作。在2级DOM API中,可以调用preventDefault()方法,与在0级事件模型中返回false一样。
19.2.6.2 UIEvent
UIEvent接口是Event接口的子接口。它定义的事件对象类型要传递给DOMFocusIn,DOMFocusOut和DOMActivate类型的事件。这些事件类型不常用。关于UIEvent接口更重要的是,它是MouseEvent接口的父接口。除了Event接口定义的属性外UIEvent接口还定义了两个属性。
view:发生事件的Window对象(在DOM术语中称为“视图”)。
detail:一个数字,提供事件的额外信息。对于click事件,mousedown事件和mouseup事件,这个字段代表点击的次数,1代表点击一次,2代表双击,3代表点击三次(注意,每次点击生成一个事件,但如果多次点击的间隔足够近,就可以用detail值说明它。简而言之,detail值为2的鼠标事件,前面总是有一个detail值为1的鼠标事件)。对于DOMActivate事件,这个字段的值为1,表示正常激活,2表示超级激活,如双击鼠标或同时按下Shift键和Enter键。
19.2.6.3 MouseEvent
MouseEvent接口继承Event接口和UIEvent接口的所有属性和方法,此外它还定义了下列属性:
button:一个数字,声明在mousedown,mouseup和click事件中,哪个鼠标键改变了状态。值为0表示左键,1表示中间键,2表示右键。这个属性只在鼠标状态改变时使用,例如,在mousemove事件中,它不能用来汇报按键是否被按下并保持信了。还要注意,Netscape 6弄错了它的值,不是用0,1和2表示,而是用1,2,和3来表示,Netscape 6.1修正了这个问题。
altKey,ctrlKey,metaKey和shiftKey:这四个布尔值声明在鼠标事件发生时,是否按住了Alt键,Ctrl键,Meta键或Shift键。与button属性不同,这些键盘属性对任何鼠标事件类型都有效。
clientX,clientY:这两个属性声明鼠标指针相对于客户区或浏览哭窗口的X坐标和Y坐标。注意,这两个坐标不考虑文档滚动,如果事件生在窗口的顶部,元论文档滚动了多远,clientY都是0。但是2级DOM没有提供把窗口坐标转换成文档坐标的标准方法。在Netscape 6中,加上window.pageXOffset和window.pageYOffset即可。在Internet Explorer中,加上document.body.scrollLeft和document.body.scrollTop即可。
screenX,screenY:这两个属性声明了鼠标指针相对于用户显示器的左上角的X座标和Y座标。如果你计划在鼠标事件所在地(或附近)打开一个新浏览哭窗口,这两个值非常有用。
relatedTarget:该属性引用与事件的目标节点相关的节点。对于mouseover事件来说,它是鼠标移到目标上时所离开的那个节点。对于mouseout事件,它是离开目标时,鼠标进入的节点。对于其他在型的事件来说,这个属性没有用。
19.2.6.4 MutationEvent
MutationEvent接口是Event接口的子接口,用于为mutationEvents模块定义的事件类型提供事件的详细信息。在DHTML程序设计中,这类事件不常用,所以这里没有提供有关该接口的细节。详情可参阅DOM参考手册。
19.2.7 例子:拖移文档元素
现在我们已经讨论过事件传播、事件处理程序注册和2级DOM事件模型中的各种事件对象接口,最后我们看一看它们是如何使用的。例19-2展示了一个javascript函数beginDrag(),当从mousedown事件处理程序调用它时,它允许用户拖动文档元素。
beginDrag()函数有两个参数。第一个参数是要拖动的元素,它可以是发生mousedown事件的元素或包容元素(例如,允许用户拖住窗口的标题栏移动整个窗口)。但无论哪种情况,它都必须引用一个文档元素,该元素用CSS position性质绝对定位。在style性质中,CSS性质left和top必须被明确地设置为像素值。第二个参数是与触发mousedown事件相关的事件对象。
beginDrag()函数将记录mousedown事件的位置,然后为其后将要发生的mousemove事件和mouseup事件注册事件处理程序。mousemove事件的处理程序用于移动文档元素,mouseup事件的处理程序用于注销它自身和mousemove处理程序。注意,mousemove和mouseup事件的处理程序被注册为捕捉事件处理程序,因为用户移动鼠标的速度比跟随它移动的文档元素快,所以其中一些事件发生在原始目标元素外部。另外要注意,可以注册moveHandler()函数和upHandler()函数来处理定义为函数的事件,这些函数嵌套在beginDrag()中。因为它们定义在嵌套作用域中,所以它们可以使用beginDrag()函数的参数和局部变量,这些参数和变量极大地简化了它们的实现。
例19-2:用2级DOM事件模型拖动文档元素
/**
*Drag.js:
*该函数由mousedown事件处理程序调用。
*它为随后发生的mousemove和mouseup事件注册了临时的捕捉事件处理程序,
*并用这些事件件处理程序拖动指定的文档元素。
*第一个参数必须是绝对定位的文档元素。
*它可以是接收mousedown事件的元素,也可以是包容元素。
*第二个参数必须是mouserdown事件的事件对象。
**/
function beginDrag(elementToDrag, event){
//指定出该元素当前位于何处。
//该元素的样式性质中必须有left和top CSS属性。
//此外,我们假设它们采用像素作单位。
var x = parseInt(elementToDrag.style.left);
var y = parseInt(elementToDrag.style.top);
//计算一个点和鼠标点击的位置之间的距离。
//下面嵌套的moveHandler函数需要这些值。
var deltaX = event.clientX - x;
var deltaY = event.clientY - y;
//注册mousedown事件后发生的mousermove和mouseup事件的处理程序。
//注意,它们被注册为文档的捕捉事件处理程序。
//在鼠标按钮保持被按下的状态时,这些事件处理程序保持活动状态,
//在按钮被释放时,它们被删除。
document.addEventListener("mousemove", moveHandler, true);
document.addEventListener("mouseup", upHandler, true);
//我们已经处理了该事件。不要让别的元素看到它。
event.stopPropagation();
event.preventDefault();
/**
*这是在元素被拖动时捕捉mousemove事件的处理程序。
*它响应移动的元素。
**/
function moveHandler(event){
//把元素移到当前鼠标位置,根据初始鼠标点击的偏移量进行调整。
elementToDrag.style.left = (event.clientX - deltaX) + "px";
elementToDrag.style.left = (event.clientY - deltaY) + "px";
//不要让元素看到事件。
event.stopPropagation();
}
/**
*该处理程序将捕捉拖移结束时发生的mouseup事件。
**/
function upHandler(event){
document.removeEventListener("mouseup", upHandler, true);
document.removeEventListener("mousemove", moveHandler, true);
//不要让该事件进一步传播。
event.stopPropagation();
}
}
可以采用下列方式在HTML文件中使用beginDrag()函数(它是例19-2的简化版本):
<script src="Drag.js"></script><!--包括Drag.js脚本-->
<!--定义要拖动的元素-->
<div style="position:absolute; left:100px; top:100px; background-color:white; border: solid black;">
<!--定义进行拖移的处理程序,注意onmousedown性质-->
<div style="background-color:gray; border-bottom: dotted black; padding:3px; font-family:sans-serif; font-weight:boid;" onmousedown="beginDrag(this.parentNode, event);">
Drag Me<!--标题栏的内容-->
</div>
<!--可拖动的元素内容-->
<p>This is a text.Testing, testing, testing.<p>This is a test.<p>Test.
</div>
关键在于<div>元素的onmousedown性质。虽然beginDrag()使用的是2级DOM事件模型,但为了方便,我们用0级模型注册它。在接下来的小节中,我们将讨论事件模型可以混合。在事件处理程序设置为HTML性质时,用关键字event可以访问事件对象(这不属于DOM标准,但它是Netscape 4和IE事件模型的一个规约,在后面会介绍它们)。
下面是另一个使用beginDrag()函数的简单例子,它定义了一个图象,用户只在按下并保持住Shift键时可以拖动该图像:
<script src="Drag.js"></script>
<img src="001.gif" width="20p" height="20" style="postion:absolute; left:0px; top:0px;" onmousedown="if(event.ShiftKey) beginDrag(this, event);">
注意这里的onmousedown性质和上个例子中的区别。
把地址用手机拍下来,方便随时查看!
———— 马上加微信联系:15823808970 张老师 ————
沙坪坝校区:
报名联系人:张老师
联系电话:15823808970 023-65316279
联系QQ:869488371
联系地址:重庆市沙坪坝三峡广场广电大厦四楼(王府井旁边)
南坪校区:
报名联系人:张老师
联系电话:17723551922 023-62530792
联系QQ:332334275
联系地址:南坪万达广场2号写字楼6楼11号(南坪轻轨站2号出口右侧)
江北校区:
报名联系人:陈老师
联系电话:18983942570 023-63874645
联系QQ:255678862
联系地址:江北观音桥拓展大厦18楼(新世纪百货后面)
杨家坪校区:
报名联系人:高老师
联系电话:18983940174 023-68612501
联系QQ:1840862932
联系地址:杨家坪轻轨站旁聚彩阁10-4(九龙坡人民医院A区大门正对面)