事件处理是 Ajax 应用中的重要组成部分,也是应用动态变化的源动力。本文详细介绍了浏览器中的事件处理相关的内容,包括注册事件监听器、事件发生之后的传播机制、编写事件监听器等,还介绍了 Dojo 对事件处理提供的支持。最后介绍了与浏览器内存泄露和性能相关的最佳实践。
浏览器中的事件是 Ajax 应用动态变化的源动力。用户通过输入设备(主要是键盘和鼠标)与应用进行互动。对于用户不同的动作,如点击鼠标左键、右键、或是按下键盘上的回车键,浏览器会产生与之对应的事件。这些事件按照一定的规则在当前文档树中传播。应用可以根据自己的需要,对特定的事件进行处理,以响应用户的动作。这种事件驱动的方式,不仅在 Web 应用中被使用,在桌面应用中也广泛流行。本文详细介绍了浏览器中事件处理的各个方面,包括事件监听器的注册、事件的传播、事件处理和其它高级话题。本文还介绍了如何使用 Dojo 提供的 dojo.connect()。本文中使用的 Dojo 版本是 1.4。下面首先介绍如何注册事件监听器。
注册事件监听器
注册事件监听器的目的是在事件发生的时候添加相应的处理逻辑。浏览器中的事件处理采用经典的观察者(Observer)设计模式。对于可能产生的各种事件,Ajax 应用通过脚本在节点上关注自己感兴趣的事件,并添加相应的处理逻辑。当相应的事件发生并传播到监听器注册的节点时,处理逻辑会被调用。
由于历史原因、浏览器之间的兼容性问题以及 W3C 的标准化工作,目前注册事件监听器的方式主要有三种,分别是 DOM 级别 0 定义的方式、W3C 规范定义的事件模型和 IE 独有的事件模型。下面分别对这三种方式进行详细的介绍。
DOM 级别 0 定义的方式
这种事件监听器注册方式是把事件处理方法作为 DOM 节点对象的属性来设置。设置属性就相当于为对应的事件注册了监听器。DOM 节点对象有不同的属性与不同的事件类型相对应,如 onclick对应于鼠标的点击、onsubmit对应于表单的提交、onkeydown对应于键盘上的键被按下等。
这种事件监听器注册方式由于出现得最早,有着很好的浏览器兼容性,使用起来也比较简单。它的问题是由于事件处理方法被设置为 DOM 节点对象的属性,因此对一个 DOM 节点的每种事件,最多只能有一个事件处理方法。之后设置的方法会覆盖掉之前的方法。对于一个多人开发的比较复杂的 Ajax 应用来说,这会是一个不小的问题。很可能某个开发人员添加的事件处理方法被另外的开发人员无意中覆盖,造成难以调试的问题。另外这种方式只支持事件的冒泡阶段,不支持捕获阶段。
W3C 规范定义的事件模型
在 W3C 的 DOM 级别 2 规范中引入了与浏览器事件相关的内容。这其中一个重要的接口就是 EventTarget,用来表示一个事件的目标。对于一个事件目标,可以在其上注册多个事件监听器。DOM 中的节点(Node)实现了此接口。因此,文档树中的任何节点都可以作为事件目标,从而在其上注册事件监听器。注册事件监听器是通过 EventTarget接口的 addEventListener(type, listener, useCapture)方法来完成的。该方法的参数 type表示的是事件的类型,如 click、submit和 keypress等;参数 listener表示的是事件的处理方法;参数 useCapture表示是否启用事件捕获。这三个参数都是必须的。关于事件捕获和冒泡的细节,下面章节会介绍。与 addEventListener()对应的是 removeEventListener(),用来从事件目标中删除监听器,其参数与 addEventListener()相同。
使用 W3C 规范定义的事件模型的最大好处是可以为每个节点的每个事件注册多个监听器。这些监听器不会互相影响。当事件发生的时候,这些监听器都会被触发,但是具体的顺序是不确定的。另外这也是符合标准的做法,并且同时支持事件的捕获和冒泡。不过最大的问题是 IE 并不支持此事件模型。
IE 独有的事件模型
IE 采用了与 W3C 规范不同的事件模型。该模型与 W3C 规范定义的事件模型有点类似。它使用 attachEvent(type, listener)和 detachEvent(type, listener)两个方法来完成事件监听器的注册和删除。与 W3C 规范中的 addEventListener()和 removeEventListener()方法相比,这两个方法都少了一个参数。这是由于 IE 并不支持事件的捕获。另外事件的类型也必须以“on”开头,如 onclick、onsubmit和 onkeypress等。
IE 独有的事件模型也支持为每个元素的每个事件注册多个监听器。不过该模型只在 IE 中有效,在其它浏览器中,需要使用 W3C 规范定义的事件模型。
代码清单 1 中给出了如何分别使用这三种方式为一个元素注册鼠标点击事件的监听器。
清单 1. 三种事件监听器注册方式示例
var node = document.getElementById("myDiv"); node.onclick = function() { alert("DOM 级别 0 事件注册"); }; if (node.addEventListener) { node.addEventListener("click", function() { alert("W3C 事件模型"); }, false); } if (node.attachEvent) { node.attachEvent("onclick", function() { alert("IE 事件模型"); }); }
在介绍了如何注册事件监听器之后,下面介绍事件发生之后在当前文档树中的传播方式。
事件传播
用户通过输入设备产生的动作会被浏览器捕获,浏览器产生与之对应的事件。当事件产生之后,会在当前文档树中按照一定的规则进行传播。每个事件都一个对应的目标,在文档树中有一个节点与此目标对应,称为目标节点。比如点击一个按钮,那么产生的鼠标点击事件的目标节点就是此按钮。事件的传播分成三个阶段进行:捕获阶段、目标阶段和冒泡阶段。下面分别进行说明。
捕获阶段:事件首先从文档的根节点开始,传播给目标节点的祖先节点,直到目标节点的直接父节点。
目标阶段:事件传播到目标节点。
冒泡阶段:事件从目标节点的直接父节点开始,传播到目标节点的祖先节点,直到文档的根节点。
图 1 中给出了事件传播过程的示意图。图中的箭头表示了事件的传播方向。
图 1. 事件传播过程示意图
从上面的过程可以看出,事件的传播与目标节点及其祖先节点密切相关。目标节点及其祖先节点在事件发生的 时候就确定了,并且在整个传播过程中不会改变。即便是有祖先节点被删除,传播也是根据之前确定的节点来进行。不是所有的事件传播都需要经历上面的三个阶段,可以通过脚本来取消某个阶段的传播。具体的做法在下面会提到。不同浏览器对事件传播阶段的支持也不相同。如 IE 只支持事件的冒泡阶段,而不支持捕获阶段。
在注册事件监听器的时候,可以选择在事件传播的捕获阶段还是冒泡阶段进行触发。下面分别介绍这两种情况。
注册事件监听器到捕获阶段
只有 W3C 规范定义的事件模型支持把事件监听器注册到捕获阶段。在使用 addEventListener()注册事件监听器的时候,通过把第三个参数的值设为 true,就把此监听器声明为在捕获阶段触发。该监听器可以拦截其目标节点的后代节点上的相同类型的事件,也就是说该监听器的触发是先于其后代节点上的监听器的。这样的话,该监听器就有可能改变事件的传播。代码清单 2 中给出了注册事件监听器到捕获阶段的示例。
清单 2. 注册事件监听器到捕获阶段的示例
HTML 代码片段
<div id=”parentDiv”>
<div id=”myDiv1″> 点击我 1</div>
<div id=”myDiv2″> 点击我 2</div>
</div>
JavaScript 代码
var parentNode = document.getElementById("parentDiv"); node = document.getElementById("myDiv1"); if (parentNode.addEventListener) { parentNode.addEventListener("click", function() { alert("点击父节点"); }, true); node.addEventListener("click", function() { alert("点击子节点 1"); }, false); }
如 代码清单 2 所示,在父节点 parentNode上注册了鼠标点击事件在捕获阶段的监听器,因此点击子节点的时候,父节点上的监听器方法也会被调用,而且是先于子节点上事件监听器的。因此点击 ID 为 myDiv1的子节点的时候,会依次弹出对话框“点击父节点”和“点击子节点 1”。
注册事件监听器到冒泡阶段
有三种方式可以把监听器注册到事件传播的冒泡阶段:一种是 DOM 级别 0 的方式,另外一种是 W3C 事件模型中的 addEventListener()的第三个参数的值为 false,最后一种是 IE 事件模型中的 attachEvent()。当事件传播到目标节点之后,会继续传播给其祖先节点。在这个过程中,祖先节点上注册的冒泡阶段监听器会被触发。代码清单 3 给出了注册事件监听器到冒泡阶段的示例。
清单 3. 注册事件监听器到冒泡阶段的示例
var parentNode = document.getElementById("parentDiv"), node = document.getElementById("myDiv1"); if (parentNode.addEventListener) { parentNode.addEventListener("click", function() { alert("点击父节点(捕获阶段)"); }, true); parentNode.addEventListener("click", function() { alert("点击父节点(冒泡阶段)"); }, false); node.addEventListener("click", function() { alert("点击子节点 1"); }, false); }
代码清单 3 中给出了示例的 JavaScript 代码,HTML 代码片段与 代码清单 2 中的是相同的。当点击 ID 为 myDiv1的节点时,首先父节点上的捕获阶段的监听器被调用,接着是目标节点上的监听器,最后是父节点上冒泡阶段的监听器。因此会依次弹出对话框“点击父节点(捕获阶段)”、“点击子节点 1”和“点击父节点(冒泡阶段)”。
当事件在各个节点上按照上述过程传播的时候,可能触发节点上注册的事件监听器。事件监听器被触发需要满足三个条件:
事件的类型与注册时指定的类型相同。
事件当前的传播阶段与注册时指定的阶段相同。在冒泡阶段的事件并不会触发注册到捕获阶段的监听器。
DOM 级别 3 规范引入了事件监听器分组的概念。一个事件监听器可能阻止同组的事件监听器被触发,而其它组的事件监听器不受影响。一般可以忽略这个条件。
在介绍了事件的传播过程之后,下面介绍在事件监听器中处理发生的事件。
事件处理
注册事件监听器的目的是在感兴趣的事件发生的时候,执行所需的应用逻辑。事件监听器是一个 JavaScript 方法。事件监听器被触发即此 JavaScript 方法被调用。在处理事件的时候,需要获取与此事件相关的上下文信息。这些信息是保持在一个事件对象中的。
事件对象
事件对象中包含了与事件相关的上下文信息,如事件类型和目标节点等。不同类型的事件所包含的信息也不相同,如鼠标点击事件包含鼠标点击的位置,而键盘事件包含按键的编码以及是否同时按下了 Shift、Alt 和 Ctrl 等键。
获取此事件对象的方式在 IE 和其它浏览器上是不同的。IE 把此对象存放在全局的 window.event变量中,而其它浏览器则把此对象作为事件监听器 JavaScript 方法调用时的第一个参数。不同的浏览器中事件对象包含的属性也是不同的。同样含义的属性其名称也可能不同。如 W3C 规范中表示事件目标节点的属性是 target,而 IE 使用的是 srcElement。在下面介绍具体的浏览器事件时,会介绍其事件对象所包含的详细内容。
this 所引用的对象
在编写事件监听器的 JavaScript 方法的时候,很可能需要用到 JavaScript 中的 this关键词。如果不显式指定的话,this所引用的对象会根据事件监听器注册的方式而有所不同。
DOM 级别 0 定义的方式:this指向的是当前节点。
W3C 规范定义的事件模型:this指向的是当前节点。
IE 独有的事件模型:this指向的是全局对象,即 window对象。
从上面可以看到,IE 采用了与众不同的做法,而这种做法并不符合开发人员一般的理解。为了避免这种不一致可能带来的问题,一种做法是显式指定 this所引用的对象,可以通过 dojo.hitch()来实现;另外一种做法是对 IE 做特殊处理,使其符合 W3C 规范的标准做法。很多 JavaScript 框架都做了这种处理。
阻止事件传播
前面介绍了事件传播过程的三个阶段,事件监听器的处理方法中可以通过脚本来阻止事件的传播过程。这样的话可以阻止其它事件监听器被触发。W3C 规范定义的事件模型中,事件对象的方法 stopPropagation()可以用来阻止事件的继续传播。如果一个节点上注册有多个事件监听器,其中一个事件监听器调用了 stopPropagation()来阻止事件传播,该节点上的其它事件监听器并不会受到影响,仍然会被触发。受影响的是事件传播路径上的后续节点上的事件监听器。W3C 规范定义的事件模型支持事件的捕获和冒泡两个阶段,stopPropagation()对两个阶段都有影响。对 IE 独有的事件模型来说,阻止事件传播是通过设置事件对象的 cancelBubble属性来完成的。代码 window.event.cancelBubble = true;会阻止事件的冒泡阶段。IE 也只支持冒泡阶段。代码清单 4 给出了阻止事件传播的示例
清单 4. 阻止事件传播的示例
var parentNode = document.getElementById("parentDiv"), node = document.getElementById("myDiv1"); if (parentNode.addEventListener) { parentNode.addEventListener("click", function(e) { alert("点击父节点(捕获阶段)"); e.stopPropagation(); }, true); node.addEventListener("click", function() { alert("点击子节点 1"); }, false); } if (parentNode.attachEvent) { node.attachEvent("onclick", function() { alert("点击子节点 1"); window.event.cancelBubble = true; }); parentNode.attachEvent("onclick", function() { alert("点击父节点(冒泡阶段)"); }); }
如 代码清单 4 所示,对 W3C 规范定义的事件模型和 IE 独有的事件模型采用了不同的处理方法。父节点 parentNode上的捕获阶段的事件监听器阻止了事件的传播,因此点击子节点之后,事件传播到父节点上,其上的事件监听器被触发,但是子节点上事件监听器并不会被触发。IE 事件模型上的处理与此类似,不同的是发生在冒泡阶段。在 IE 上只会弹出对话框“点击子节点 1”,在其它浏览器上只会弹出对话框“点击父节点(捕获阶段)”。
阻止浏览器默认行为
对于某些事件来说,浏览器会有对应的默认行为。比如点击一个超链接的时候,浏览器会打开此链接。该点击动作也会触发此“a”元素上注册的 click事件的监听器。如果希望阻止浏览器默认行为的方式,W3C 规范定义的事件模型中可以调用事件对象的 preventDefault()方法来实现。IE 独有的事件模型中可以通过设置事件对象的 returnValue属性的值为 false来实现。
在介绍完事件处理相关的内容之后,下面介绍 Dojo 提供的 dojo.connect()方法。
dojo.connect
dojo.connect()是 Dojo 提供的通用的事件处理方法。使用它可以为任何 JavaScript 方法或 DOM 事件添加监听器。当该 JavaScript 方法被调用或是 DOM 事件发生的时候,监听器都会被触发。对于 DOM 事件来说,dojo.connect()方法为开发人员解决了不同浏览器之间的兼容性问题。使用它编写事件监听器变得更加容易。dojo.connect()的方法声明是 dojo.connect(obj, event, context, method, dontFix),其中参数 obj表示 JavaScript 方法所在的对象或是 DOM 节点;event表示 JavaScript 方法的名称或是 DOM 事件的类型;context表示监听器方法调用时 this所指向的对象;method表示监听器方法;如果 obj是 DOM 节点的话,dontFix的值为 true表示不把此事件代理给浏览器处理。
用 dojo.connect()进行事件注册的好处在于它很好的解决了不同浏览器之间的兼容性问题。首先是事件监听器方法中 this所指向的对象,通过 dojo.connect()的参数 context就可以进行指定。另外一个是事件对象中所包含的信息。Dojo 对 IE 独有的事件模型的事件对象做了处理,使其符合 W3C 规范定义的事件模型。事件对象总是作为事件监听器方法的第一个参数出现。由于 IE 只支持事件传播的冒泡阶段,dojo.connect()是把事件监听器添加到事件传播的冒泡阶段。如果想添加到捕获阶段,需要直接使用 addEventListener()。代码清单 5 中给出了使用 dojo.connect()注册事件监听器的示例。
清单 5. 使用 dojo.connect()注册事件监听器的示例
dojo.connect(dojo.byId("parentDiv"), "click", function(e) { alert(e.target.tagName); });
如 代码清单 5 所示,使用 dojo.connect()进行事件注册的代码非常简单,并且对主流浏览器都适用。dojo.connect()返回的是当前事件绑定的标识符,可以通过 dojo.disconnect()来取消此绑定。Dojo 提供了 dojo.stopEvent()方法来阻止事件传播和浏览器的默认行为,该方法惟一的参数是事件对象。
在介绍完 dojo.connect()方法之后,下面介绍浏览器中几类重要的事件。
几类重要的事件
下面重点介绍几类浏览器中的常见事件,包括鼠标、键盘和页面事件。
鼠标事件
与鼠标相关的事件主要有 click、mousedown、mouseup、mouseover、mousemove和 mouseout,分别表示鼠标点击、鼠标键按下、鼠标键释放、移动到元素上、在元素上移动和移动出元素。包含这些事件上下文信息的事件对象是在 DOM 级别 2 规范中标准化的,其中包含的信息如 表 1 所示。
表 1. 鼠标事件中的属性
属性 | 说明 |
---|---|
screenX/screenY | 分别表示事件发生时鼠标位置在屏幕坐标系统中的 X 轴和 Y 轴上的位置。 |
clientX/clientY | 分别表示事件发生时鼠标位置在浏览器窗口坐标系统中 X 轴和 Y 轴上的位置。 |
pageX/pageY | 分别表示事件发生时鼠标位置在当前页面坐标系统中 X 轴和 Y 轴上的位置。这两个属性与 clientX/clientY的区别在于,页面的大小可能超过浏览器窗口而出现滚动条。 |
layerX/layerY | 分别表示事件发生时鼠标位置相对于事件目标在 X 轴和 Y 轴上的位置。 |
relatedTarget | 表示与事件相关的另外一个事件目标节点。目前只使用在 mouseover和 mouseout事件中,分别表示鼠标离开的节点和鼠标进入的节点。 |
除了 表 1 中给出的属性之外,鼠标事件对象还包含与鼠标按键相关的信息。Dojo 提供了一些帮助方法用来检测鼠标按键:dojo.mouseButtons.isLeft(e)、dojo.mouseButtons.isMiddle(e)和 dojo.mouseButtons.isRight(e)分别用来检测鼠标左键、中键和右键,其参数 e表示的是事件对象。
键盘事件
与键盘相关的事件有 keydown和 keyup,分别键盘上的键被按下和释放。键盘事件是在 DOM 级别 3 规范中标准化的,其中包含的信息如 表 2 所示。
表 2. 键盘事件中的属性
属性 | 说明 |
---|---|
keyCode | 表示键盘上按键的编码。 |
charCode | 表示键盘上按键对应的字符编码。 |
altKey/ctrlKey/shiftKey | 分别表示键盘事件发生时,键 Alt/Ctrl/Shift 是否被同时按下。 |
在键盘事件的监听器方法中,一般是通过 keyCode来判断按下或是释放的键。在不同的浏览器上,同样的按键的编码可能不同。Dojo 的 dojo.keys中提供了浏览器兼容的按键编码。比如需要检测是否按下了回车键,最好用 e.keyCode == dojo.keys.ENTER来实现。
页面事件
与页面相关的事件主要有 load、unload、resize和 scroll,分别表示页面加载完成、用户离开当前页面、页面大小发生改变和在页面内发生滚动。由于前两种事件比较常用,Dojo 提供了方法 dojo.addOnLoad()和 dojo.addOnUnload()来方便注册这两种事件的监听器。
在介绍完几类重要的事件之后,下面介绍一些高级话题。
高级话题
在介绍完浏览器中事件相关的基本内容之后,下面讨论一些高级话题。首先从避免浏览器中的内存泄露开始。
避免内存泄露
DOM 的事件处理通常与浏览器中的内存泄露联系在一起,尤其在 IE 中。在事件监听器方法中很容易不正确的使用闭包,造成 DOM 节点对象和 JavaScript 对象之间的循环引用。IE 的垃圾回收机制不能正确处理这种循环引用,从而导致内存泄露。代码清单 6 中给出了一种典型的内存泄露模式。
清单 6. 典型的浏览器内存泄露模式
function addEvent(element) { function handler() { alert("点击!"); } element.attachEvent("onclick", handler); } addEvent(document.getElementById("myDiv"));
在 代码清单 6 中,通过 element.attachEvent()方法,DOM 节点对象引用了 JavaScript 方法 handler,而方法 handler由于在外部方法 addEvent之内,它有外部方法 addEvent的参数 element的引用。这样就形成了循环引用,其结果是 DOM 节点对象 element和 JavaScript 方法 handler所占用的内存都不会被释放。为了避免造成内存泄露,下面提供几条比较好的实践经验。
使用 dojo.connect()注册事件监听器。这样不仅兼容性更好,而且减少了无意中创建的闭包,不容易造成内存泄露。
事件监听器方法应该尽可能的简单。方法体的代码应该尽可能的少,把业务逻辑代理给其它方法来完成。这样做的好处是容易识别出其中的对象引用,便于检查是否有内存泄露。
尽量不要在事件监听器方法中为 DOM 节点对象添加额外的属性。有时候为了维护状态,可能会把一些属性存放在 DOM 节点对象中,如 document.getElementById(“myDiv”).myVal = “A string.”;。这样做的缺点是如果该 DOM 节点对象无法被回收,这些额外的属性也会无法被回收,造成更多的内存泄露。应该考虑使用其它的维护状态的做法,比如状态只保存在与 DOM 节点无关的普通 JavaScript 对象中,或是把状态保存在 DOM 节点的属性中。即便把额外的属性添加到 DOM 节点对象中,也尽可能只添加基本数据类型的属性,即字符串、布尔值和数字值等。如果添加 JavaScript 方法到 DOM 节点对象中,有很大可能会由于编程错误造成内存泄露。
显式的删除对象引用。在有些情况下,通过显式地把对象引用设成 null,或是调用 delete来删除。这样的好处是可以显式的解除循环引用。
性能
用户在使用 Ajax 应用中,会产生相当多的事件。高效的处理这些事件,是提高用户体验的一个重要因素。虽然事件的处理时间在很大因素上取决于要完成的业务逻辑,但还是有一些比较好的实践能够提高事件处理的性能。
第一条实践是减少事件监听器的注册,充分利用事件的传播机制。由于 IE 只支持事件的冒泡阶段,一般都是只针对事件的冒泡阶段。根据事件的冒泡方式,子节点上发生的事件会传播到其祖先节点上。如果需要为多个子节点添加逻辑相同的事件监听器,更好的做法是把监听器添加到其某个祖先节点上。这样的话就会减少事件监听器的数量。代码清单 7 中给出了一个示例。
清单 7. 高效注册事件监听器的示例
HTML 代码片段
<div id=”parentDiv”>
<div> 子节点 1</div>
<div> 子节点 2</div>
<div> 子节点 3</div>
</div>
JavaScript 代码
dojo.connect(dojo.byId("parentDiv"), "click", function(e) { var target = e.target; if (dojo.hasClass(target, "item")) { alert(target.innerHTML); } });
在 代码清单 7 中,事件监听器注册在父节点 parentDiv上,当事件发生的时候,首先需要判断事件目标节点是不是感兴趣的子节点。如果是的话,就执行对应的业务逻辑。这样的判断是必须的,是为了保证处理的事件发生在正确的节点上。如果把事件监听器注册在子节点上的话,则需要三个监听器,从而带来更大的开销。
第二条实践是适时的阻止事件传播。在开发某个组件的时候,在其 DOM 节点的事件监听器完成处理之后,就可以调用 dojo.stopEvent(e)来阻止事件继续传播。这样做的好处是避免事件继续传播,错误地触发其它节点上的事件监听器。当 Ajax 应用比较复杂,并且其中包含的组件比较多的时候,适时的阻止事件传播可以减少组件间的互相影响。
文章收集整理自互联网,版权归原作者所有。
上一篇: 关于借钱的故事 下一篇: 初识Apache Velocity(Java模板引擎)