17.2. DOM2中的高级事件处理(Advanced Event Handling with DOM Level 2)
译自:JavaScript: The Definitive Guide, 5th Edition ----By David Flanagan
迄今为止,在本章中出现的事件处理技术都是DOM0级的一部分,所有支持JavaScript的浏览器都支持DOM0的API.DOM2定义了高级的事件处理API,和DOM0的API相比,有着令人瞩目的不同(而且功能更强大).虽然DOM2标准并没有把已有的API收入其中,但是DOM0级API也没有被去除.对于基本的事件处理任务,你会觉得使用这些简单的API更自由一些.
DOM2事件模型被除了IE以外的所有浏览器支持.
17.2.1. 事件传播(Event Propagation)
在DOM0级事件模型中,浏览器分派事件给发生事件的文档元素.如果那个对象有相应的事件处理程序,那么就运行该程序.再没有更多的事情发生了.情况在DOM2中就是复杂得多.在DOM2高级事件模型中,当一个文档元素(被叫做事件的目标(target)对象)触发了一个事件,这个目标对象的事件处理程序被触发,除此之外,该目标对象的每一个祖辈元素都有一个或者两个机会去处理该事件.事件传播的过程包括三个阶段.
首先,在捕获阶段(capturing phase),事件是从文档对象(Document object)开始,沿着文档树向下一直到目标对象传播的.如果任何目标对象的祖辈(不包括目标对象本身)也有一些指定注册的捕获事件的处理程序,在事件传播的这个阶段(捕获阶段)将运行它们.(一会儿你就会看到如何注册正常事件处理程序和捕获事件处理程序.)
事件传播的下一个阶段发生在目标对象自身:所有注册到目标对象的对应事件处理程序都被运行.这和DOM0提供的事件模型是相似的.
事件传播的第三阶段是冒泡阶段,或者说按文档层次倒序的,从目标元素到文档对象(Document object).尽管所有的事件都受事件传播的捕获阶段(capturing phase)的影响,但是,并不是所有类型的事件都冒泡:例如,除了被定义了提交事件(submit)的form以外,把这个事件向上传播到文档元素是没有任何意义的.另一方面,像mousedown这样的一般事件对文档中的其它元素是有意义的,所以这些事件才沿着文档层次向上冒泡,并且触发目标元素的祖辈元素的相应事件的处理程序.通常情况下,原始的输入事件冒泡,而高级的语义事件不会.(稍候在本章中出现的表17-3是一个权威的列表,它指出哪些事件是冒泡的,哪些不是.)
在事件传播期间,每个事件的处理程序都可以阻止事件的进一步传播,只需通过调用表现这个事件的事件对象的stopPropagation( )方法就可以.事件对象和stopPropagation( )方法一会儿再进一步讨论.
一些事件会使浏览器执行一个与事件相关联的默认行为.例如,当点击一个链接(<a> tag)的时候,浏览器的默认行为是转向超链接.像这样的默认行为,只有事件传播的三个阶段都完成了才会执行,在事件传播过程中调用的任何处理程序都能阻止默认行为的发生,调用事件对象的preventDefault( )方法就可以了.
尽管这种事件传播机制似乎让人难以理解,但是它有助于你集中你的事件处理代码.DOM1指出了所有的文档元素,还指出了在那些元素上允许发生的事件(如mouseover事件).这就意味着,与旧的DOM0事件模型相比,DOM1是在很多很多的地方注册事件处理程序.假设你想在鼠标经过每一个文档中的段落元素(<P>)时触发一个事件处理程序.除了在所有的段落标签(<p>tag)注册一个onmouseover事件处理程序之外,取而代之的办法是为文档对象(Document object)注册一个单独的事件处理程序,然后,或者在捕获阶段,或者在冒泡阶段,处理这些事件.
事件传播还有一个很重要的细节.在DOM0模型中,你只能为一个特定的对象的一个特定类型的事件注册一个处理程序.而在DOM2模型中,你可以为一个特定对象的一个特定类型事件注册任意数量的事件处理程序.这也适用于事件传播时,在捕获阶段或者冒泡阶段,事件对象的祖辈的处理函数被调用的情况.
17.2.2. 事件处理程序的注册(Event Handler Registration)
在DOM0的API中,你通过在HTML中设置属性(attribute)或者在JavaScript中设置一个对象的属性(property)的方法注册事件.而在DOM2模型中,你通过调用那个对象的addEventListener( )方法注册事件处理程序.(DOM标准在这个API里使用了术语listener,但是在本文中,我将继续使用这个术语的同义词:handler.)这个方法有三个参数.第一个是被注册的事件类型.事件类型是一个字符串,包含小写的,去掉开头的"on"的HTML中的事件属性名.如果你在DOM0里使用HTML属性onmousedown,你在DOM2中就是使用字符串"mousedown".
第二个参数是指定类型的事件触发的时候应该调用的监听函数.在你的函数被调用的时候,传入了唯一的一个参数:Event对象.这个对象包含了事件的细节信息(如:哪个鼠标键被按下)和一些方法,如:stopPropagation( ).在本章后面将深入讨论事件接口和它的子接口.
addEventListener( )函数的最后一个参数是一个布尔值.如果为true,指定的事件处理程序在事件传播的捕获阶段将捕获事件.如果是false,事件处理程序就是一个正常的事件处理程序了,只有在事件直接发生在该对象上或者发生在子代对象上向上冒泡到达这个元素时,处理程序才被调用.
举个例子,你可以像下面这样使用addEventListener( )方法给一个form元素注册一个提交事件(submit):
document.myform.addEventListener("submit",
function(e) {return validate(e.target); }
false);
如果你想捕获发生在一个特定名字的div中的鼠标按下(mousedown)事件,你可以这样使用addEventListener( ):
var mydiv = document.getElementById("mydiv");
mydiv.addEventListener("mousedown", handleMouseDown, true);
注意,这些例子假设你已经在你的JavaScript代码中定义了函数名为validate( )和handleMouseDown( )的函数.
用addEventListener( )函数注册的事件监听程序运行在它们被定义的作用域.它们并不是在参数的作用域链中被调用的.
因为在DOM2中通过调用一个方法来给对象添加事件监听器,而不是通过设置HTML属性或者JavaScript属性的方法,所以,你可以给一个指定对象的一个特定事件注册多于一个的事件监听程序.如果你通过调用addEventListener( )函数为同一对象的同一事件注册多个监听程序,当那个对象上那个类型的事件发生的时候(或者是向上冒泡,或者是捕获的),所有的处理程序都被调用.重点理解一下:DOM标准并没有保证一个对象的所有监听函数被调用的顺序,因此,你不应该依赖于函数按照被注册的顺序被执行(事实上是根本不按顺序执行).还要注意的是,如果你多次注册相同的监听程序给同一个元素,只有第一次注册的有效,其余的被忽略.
为什么你想要在同一个对象的同一个事件上注册多个事件监听程序呢?因为这有助于将你的软件模块化.假设,你写了一个可重用的JavaScript代码模块,它使用图像上的mouseover事件执行图片轮换.现在再假设你有另一个模块也想使用mouseover事件来显示一些在HTML弹出窗体或者工具提示(Tool tip)上的附加信息.在DOM0的API中,你不得不把这两段代码合并成一个,这样才能共用一个图片对象的onmouseover属性.另一方面,在DOM2的API中,每一个模块都可以注册它需要的事件监听程序,而不必管其它的模块.
removeEventListener( )和addEventListener( )是一对方法,它需要与addEventListener( )同样的三个参数,但它的功能是从一个对象删除一个事件监听函数,而不是添加.它常用于临时注册一个事件监听函数,然后很快就删除这个函数.例如,你得到一个mousedown事件的时候,想为mousemove和mouseup事件注册临时的捕获事件监听函数,这样就可以知道,是否用户拖拽了鼠标.然后在mouseup事件发生的时候,解除这个注册的监听程序.在这种情况下,事件监听器的删除代码如下:
document.removeEventListener("mousemove", handleMouseMove, true);
document.removeEventListener("mouseup", handleMouseUp, true);
addEventListener( )方法和removeEventListener( )方法都被定义在事件目标接口中(the EventTarget interface),在支持DOM2事件模型的Web浏览器中,元素和文档节点实现了这个接口,并且提供了这些事件注册方法.
[*] 从技术上来讲,DOM指出在文档(document)中的所有节点(包括文本节点:Text nodes)都实现了这个事件对象接口.然后事实上,web浏览器仅在元素(Element)和文档节点(Document nodes)上支持事件监听器的注册,还有窗口(Window)对象,尽管这已经超出了DOM的范围.
17.2.3. addEventListener( )和this关键字(addEventListener( ) and the this Keyword)
在原来的DOM0级事件模型中,当一个函数被注册给一个文档元素的某个事件监听程序时,它变成了那个文档元素的一个方法.当这个事件监听程序被调用时,它作为这个元素的一个方法被调用,在函数的内部,this关键字引用当前发生事件的元素.
DOM2是用一种与语言无关的方法写的,它指出监听器(listeners)是对象,而不是简单的函数.绑定了DOM的JavaScript用Javascript函数事件监听器取代对JavaScript对象使用的需求.( The JavaScript binding of the DOM makes JavaScript functions event handlers instead of requiring the use of a JavaScript object.)不幸的是,这个绑定关系并没有实际的指出监听函数如何被调用,也没有指出this关键字的值.
且不去考虑标准的不足,所有已知的实现都调用用addEventListener( )方法注册的处理程序,就像这些处理程序是目标对象的方法一样.也就是说,当监听程序被调用的时候,this关键字引用这个监听程序被注册的那个对象.如果你宁愿不依赖这种未指定的行为,你可以使用传入监听程序的事件对象(Event object)的currentTarget属性.在本章稍候的讨论中你会看到,currentTarget属性引用事件监听程序被注册的对象.
17.2.4. 把对象(Objects)注册为事件监听器(Registering Objects as Event Handlers)
addEventListener( )允许你注册一个事件监听函数.对于面向对象编程,你可能更喜欢定义一个客户端对象的方法作为事件监听程序,然后把它们作为那个对象的方法进行调用.对于Java程序员,DOM标准允许这样做:事件监听程序可以是实现了EvnentListener接口并且有一个名为handleEvent()的方法的对象.在Java中,当你注册一个事件监听程序时,你给addEventListener( )传入一个对象,而不是一个函数.简单的说,绑定了DOM API的JavaScript不需要你去实现EventListener接口,相反的,允许你直接给addEventListener( )传递一个函数引用.
然而,如果你在写一个面向对象的JavaScript程序,并且更喜欢用对象作为事件监听程序,你可以用一个像下边这样的函数来注册:
function registerObjectEventHandler(element, eventtype, listener, captures) {
element.addEventListener(eventtype,
function(event) { listener.handleEvent(event); }
captures);
}
只要一个对象定义了handleEvent( )方法,就可以用这个函数把该对象注册为一个事件监听程序.那个方法作为监听对象的方法被调用,this关键字引用这个监听对象,而不是产生事件的文档元素.
尽管这不是DOM标准的一部分,Firefox(和其它基于Mozilla codebase的浏览器)允许把定义了handleEvent()方法的事件监听对象直接传递给addEventListener()方法,来代替函数.对于这些浏览器,就没有必要定义一个像刚才展示的注册函数了.
17.2.5. 事件模型和事件类型(Event Modules and Event Types)
如我前面所说,DOM2是模块化的,所以,一个实现可以支持其中的一部分而忽略其它对其它部分的支持.事件API(Events API)就是这样一个模块.你可以像这样来测试一个浏览器是否支持这个模块:
document.implementation.hasFeature("Events", "2.0")
然而,事件模块只包含用于基本事件监听结构的API.子模块提供对特定类型事件的支持.每个子模块都提供对一类相关事件类型的支持,并且定义了传入事件监听程序的事件类型.例如,名为MouseEvents的子模块提供了mousedown, mouseup, click等相关事件的类型.它也定义了MouseEvent接口.实现了那个接口的对象,为任何一个被这个模块支持的事件类型,被传入事件监听程序.
表17-2列出了每一个事件模块,它定义的接口,和被它支持的事件类型.注意,DOM2并没有把任何键盘事件标准化,因此这里没有列出键盘事件模块.然而,当前的浏览器都支持键盘事件,在本章的后面,你会了解的更多一些.(此处省略几句和MutationEvents模块相关的描述)
Table 17-2. Event modules, interfaces, and types
Module name |
Event interface |
Event types |
---|---|---|
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 |
如你在表17-2中所见,HTMLEvents和MouseEvents模块定义的事件类型和DOM0的事件模块是非常相似的.UIEvents模块定义了事件类型,这和被HTML表单元素支持的focus,blur和click事件很相似,但更通用的,所以,他们能被任何可以接受焦点或者被激活的文档元素产生.
如前所述,当一个事件发生的时候,它的监听程序被传入一个实现了那个类型事件的事件接口对象.这个对象的属性提供了对事件监听程序可能有用的细节信息.表17-3再一次列出标准的事件,但这次是按事件类型组织的,而不是事件模型.对于每个事件类型,该表都指出传入它的监听程序的事件对象的种类,是否这个事件有一个可以用preventDefault()方法阻止发生的默认行为.对于HTMLEvents模块中的事件,表格中的第五列指出哪些HTML元素可以产生该事件.对于所有其它的事件类型,第五列指出事件对象的哪些属性包含了有意义的事件细节信息.注意,在这一列中列出的属性,不包括被基本事件接口定义的对所有事件类型都有意义的属性.
Table 17-3. Event types
Event type |
Interface |
B |
C |
Supported by/detail properties |
---|---|---|---|---|
abort |
Event |
yes |
no |
<img>, <object> |
blur |
Event |
no |
no |
<a>, <area>, <button>, <input>, <label>, <select>, <textarea> |
change |
Event |
yes |
no |
<input>, <select>, <textarea> |
click |
MouseEvent |
yes |
yes |
screenX, screenY, clientX, clientY, altKey, ctrlKey, shiftKey, metaKey, button, detail |
error |
Event |
yes |
no |
<body>, <frameset>, <img>, <object> |
focus |
Event |
no |
no |
<a>, <area>, <button>, <input>, <label>, <select>, <textarea> |
load |
Event |
no |
no |
<body>, <frameset>, <iframe>, <img>, <object> |
mousedown |
MouseEvent |
yes |
yes |
screenX, screenY, clientX, clientY, altKey, ctrlKey, shiftKey, metaKey, button, detail |
mousemove |
MouseEvent |
yes |
no |
screenX, screenY, clientX, clientY, altKey, ctrlKey, shiftKey, metaKey |
mouseout |
MouseEvent |
yes |
yes |
screenX, screenY, clientX, clientY, altKey, ctrlKey, shiftKey, metaKey, relatedTarget |
mouseover |
MouseEvent |
yes |
yes |
screenX, screenY, clientX, clientY, altKey, ctrlKey, shiftKey, metaKey, relatedTarget |
mouseup |
MouseEvent |
yes |
yes |
screenX, screenY, clientX, clientY, altKey, ctrlKey, shiftKey, metaKey, button, detail |
reset |
Event |
yes |
no |
<form> |
resize |
Event |
yes |
no |
<body>, <frameset>, <iframe> |
scroll |
Event |
yes |
no |
<body> |
select |
Event |
yes |
no |
<input>, <textarea> |
submit |
Event |
yes |
yes |
<form> |
unload |
Event |
no |
no |
<body>, <frameset> |
DOMActivate |
UIEvent |
yes |
yes |
detail |
DOMFocusIn |
UIEvent |
yes |
no |
none |
DOMFocusOut |
UIEvent |
yes |
no |
none |
被DOM0和DOM2支持的事件类型大体相同(UIEvents除外).DOM2标准添加了对abort,error,resize和scroll事件类型的支持,这些不是HTML 4的标准,但它不支持HTML 4标准中的键盘事件和双击事件.(取而代之的是,传入click事件处理程序的对象的细节属性指出了发生的连续点击的次数,在event的detail属性中.)
17.2.6. 事件接口和事件细节(Event Interfaces and Event Details)
一个事件发生的时候,DOM2提供了关于这个事件的其它细节(例如何时何地发生的),这些信息作为传入到事件监听程序的对象的属性出现.每一个事件模块都有一个相关联的事件接口,该接口指出和那个类型事件相关的细节.表17-2列出了三种不同的事件模块和三种不同的事件接口.
事实上这三个接口是互相关联的,并且形成了一个层次关系.事件接口是这个层次的根层;所有的事件对象都实现了它大部分基本的事件接口.UIEvent是事件接口的子接口:任何实现了UIEvent接口的事件对象都实现了Event接口的属性和方法.MouseEvent接口又是UIEvent的子接口.举个例子,这就是说,传给点击事件处理程序的事件对象实现了在MouseEvent, UIEvent, 和Event接口中定义的所有属性和方法.
接下来的部分介绍的是事件接口,并且着重讲最重要的属性和方法.
17.2.6.1. 事件(Event)
在HTMLEvents模块中定义的事件类型使用Event接口.所有其它的事件类型都使用Event的子接口,子接口被所有的事件对象实现,并且提供了应用于那个事件类型的细节信息.事件接口定义了如下属性(注意,这些属性和所有事件子接口的属性都是只读的):
type
发生的事件类型.这个属性的值是事件类型的名字,它和用于注册事件处理程序时使用的字符串是一样的(如: click 或者 mouseover).
target
事件发生的节点,可能与currentTarget不同.
currentTarget
正在处理事件的节点(也就是正在运行的事件处理程序所属的节点).如果在事件被捕获或者冒泡阶段被处理,这个属性的值和target属性的值是不同的.如前所述,你可以在你的事件处理程序中用这个属性代替this关键字
eventPhase
指出正在理的是事件传播的哪一个阶段的数值.该值为三个常中之一:Event.CAPTURING_PHASE, Event.AT_TARGET, 或者 Event.BUBBLING_PHASE.
timeStamp
指出该事件何时发生的日期对象
bubbles
指出该事件是否沿文档树向中冒泡的布尔值.
cancelable
指出该事件是否有一个可以用prevertDefault()阻止的和事件相关联的默认行为的布尔值.
除了这七个属性以外,事件接口定义了两个方法,也都被事件对象实现了,它们是:stopPropagation( ) 和 preventDefault( ).任何事件处理程序都可以调用stopPropagation( )来阻止正在被传播的事件越过正在被处理的节点.任何事件处理程序都可以调用preventDefault( )来阻止浏览器执行与该事件关联的默认行为.在DOM2中调用preventDefault( ),就像在DOM0中返回false.
17.2.6.2. 用户信息事件(UIEvent)
用户信息事件接口是Event的子接口.它定义了被传递到DOMFocusIn, DOMFocusOut和DOMActivate的事件对象的类型.这些事件类型通常是用不到的;但对于UIEvent接口来讲,比较重要的是它是MouseEvent的父接口.除了在Event中定义的属性外,UIEvent还定义了两个属性:
view
发生的事件所在的窗口对象(Window Object:known as a view in DOM terminology).
detail
可以提供附加信息的数值.对于click,mousedown和mouseup事件,这个字段代表点击计数:1代表单击,2代表双击,3代表三击.(注意,每次点击都产生一个事件,但是如果多次点击间隔足够短,detail属性就会指示出来.也就是说,detail为2的鼠标事件要优先于detail为1的鼠标事件.)对于DOMActivate事件,这个字段值为1代表正常激活,2代表极度活跃(hyperactivation),比如双击或者Shift-Enter组合键.
17.2.6.3. MouseEvent
MouseEvent接口继承了Event和UIEvent的属性和方法,还定义了如下附加属性:
附加属性:
button
数值类型,指出在mousedown, mouseup或者click事件期间哪一个鼠标键改变了状态.0代表左键,1代表中键,2代表右键.只有当一个按键改变状态时,才使用这个属性;例如:不能用于报告mousemove事件发生时哪个键是被按下的.注意Netscape 6得到的值为1,2,3,而不是0,1,2.这个问题在Netscape 6.1中已经修正了.
altKey , ctrlKey, metaKey, shiftKey
这四个布尔值代表,当一个鼠标事件发生时,是否 Alt, Ctrl, Meta或者Shift键被按下.与button属性不同,这些按键属性对于任何鼠标事件都是有效的.
clientX, clientY
这两个属性指出鼠标指针的X和Y坐标,相对于浏览器窗口的客户区.注意这个坐标并没有计算文档的滚动高度或者宽度在内:如果事件发生在窗口的最上边,不管这个文档已经向下滚动了多远,clientY就是0.不幸的是,DOM2并没有提供一个标准的方法去转换这个窗口坐标为文档坐标.在除了IE以外的浏览器中,你可以加上window.pageXOffset和window.pageYOffset.
screenX, screenY
这两个属性指出鼠标指针相对于用户显示器的左上角的坐标.如果你打算在鼠标事件发生的地点或者附近打开一个窗口,这两个属性就有用了.
relatedTarget
这个属性引用一个相对于事件的target节点的节点.对于mouseover事件,它引用当鼠标经过target节点时鼠标离开的那个节点.对于mouseout事件,它引用当鼠标离开目标节点时,鼠标进入的节点.对于其它事件,这两个属性是无用的.
17.2.7. 混合事件模型(Mixing Event Models)
到现在为止,我们讨论了传统的DOM0级事件模型,和新的标准的DOM2模型.为了向后兼容,支持DOM2模型的浏览器将继续支持DOM0级事件模型.这就意味着,你可以在一个文档里混合使用这两种事件模型.
支持DOM2级事件模型的web浏览器总是传递一个事件对象给事件监听程序,这和用DOM0级事件模型的HTML属性或者JavaScript属性注册事件处理程序是一致的,理解这一点很重要.当事件监听程序作为一个HTML属性被定义的时候,它被暗中转换成一个函数,这个函数有一个名为event的参数.这就意味着,像这样的一个事件监听程序可以用标识符event来引用事件对象.
DOM标准承认DOM0级事件模型继续保留使用,并指出,对待DOM0事件模型的监听程序的注册方法就像用addEventListener( )注册的一样.也就是说,如果你给一个文档元素e的onclick属性赋值为函数f(或者说设置对应的HTML中的onclick属性),它和下面这种注册方法是一样的:
e.addEventListener("click", f, false);
当函数f被调用的时候,传入一个事件对象作为参数,尽管这个函数是用DOM0模型注册的.