17.2. DOM2中的高级事件处理(Advanced Event Handling with DOM Level 2)
??????? 迄今为止,在本章中出现的事件处理技术都是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( )方法一会儿再进一步讨论.
??????? 一些事件会使浏览器执行一个与事件相关联的默认行为.例如,当点击一个链接( tag)的时候,浏览器的默认行为是转向超链接.像这样的默认行为,只有事件传播的三个阶段都完成了才会执行,在事件传播过程中调用的任何处理程序都能阻止默认行为的发生,调用事件对象的preventDefault( )方法就可以了.
??????? 尽管这种事件传播机制似乎让人难以理解,但是它有助于你集中你的事件处理代码.DOM1指出了所有的文档元素,还指出了在那些元素上允许发生的事件(如mouseover事件).这就意味着,与旧的DOM0事件模型相比,DOM1是在很多很多的地方注册事件处理程序.假设你想在鼠标经过每一个文档中的段落元素(
)时触发一个事件处理程序.除了在所有的段落标签(
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 |
, |
blur |
Event |
no |
no |
, , , , , , |
change |
Event |
yes |
no |
, , |
click |
MouseEvent |
yes |
yes |
screenX, screenY, clientX, clientY, altKey, ctrlKey, shiftKey, metaKey, button, detail |
error |
Event |
yes |
no |
<body>, <frameset>, , |
focus |
Event |
no |
no |
, , , , , , |
load |
Event |
no |
no |
<body>, <frameset>, <iframe>, , |
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 |
|
resize |
Event |
yes |
no |
<body>, <frameset>, <iframe> |
scroll |
Event |
yes |
no |
<body> |
select |
Event |
yes |
no |
, |