17.3 事件对象

在DOM中发生事件时,所有相关信息都会被收集并存储在一个名为event的对象中。
这个对象包含了一些基本信息,比如导致事件的元素、发生的事件类型,以及可能与特定事件相关的任何其他数据。例如,鼠标操作导致的事件会生成鼠标位置信息,而键盘操作导致的事件会生成与被按下的键有关的信息。
所有浏览器都支持这个event对象,尽管支持方式不同。

17.3.1 DOM事件对象

在DOM合规的浏览器中,event对象是传给事件处理程序的唯一参数。
不管以哪种方式(DOM0或DOM2)指定事件处理程序,都会传入这个event对象。
事件对象包含与特定事件相关的属性和方法。
不同的事件生成的事件对象也会包含不同的属性和方法。
所有事件对象都会包含下表列出的这些公共属性和方法:
image.png
在事件处理程序内部,this对象始终等于currentTarget的值,而target只包含事件的实际目标。
如果事件处理程序直接添加在了意图的目标,则this、currentTarget和target的值是一样的;
如果这个事件处理程序是添加到按钮的父节点(如document.body)上,那么它们的值就不一样了
type属性在一个处理程序处理多个事件时很有用
preventDefault()方法用于阻止特定事件的默认动作。
比如,链接的默认行为就是在被单击时导航到href属性指定的URL。如果想阻止这个导航行为,可以在onclick事件处理程序中取消。
任何可以通过preventDefault()取消默认行为的事件,其事件对象的cancelable属性都会设置为true。stopPropagation()方法用于立即阻止事件流在DOM结构中传播,取消后续的事件捕获或冒泡。
例如,直接添加到按钮的事件处理程序中调用stopPropagation(),可以阻止document.body上注册的事件处理程序执行。
eventPhase属性可用于确定事件流当前所处的阶段。
如果事件处理程序在捕获阶段被调用,则eventPhase等于1;
如果事件处理程序在目标上被调用,则eventPhase等于2;
如果事件处理程序在冒泡阶段被调用,则eventPhase等于3。
但要注意的是,虽然“到达目标”是在冒泡阶段发生的,但其eventPhase仍然等于2。
注:event对象只在事件处理程序执行期间存在,一旦执行完毕,就会被销毁。

17.3.2 IE事件对象

与DOM事件对象不同,IE事件对象可以基于事件处理程序被指定的方式以不同方式来访问。
如果事件处理程序是使用DOM0方式指定的,则event对象只是window对象的一个属性。

  1. var btn = document.getElementById('myBtn');
  2. btn.onclick = function() {
  3. let event = window.event;
  4. console.log(event.type); // 'click'
  5. }

window.event中保存着event对象,其event.type属性保存着事件类型(IE的这个属性的值与DOM事件对象中一样)。
但是,如果事件处理程序是使用attachEvent()指定的,则event对象会作为唯一的参数传给处理函数

  1. var btn = document.getElementById('myBtn');
  2. btn.attachEvent('onclick', function(event) {
  3. console.log(event.type); // 'click'
  4. });

使用attachEvent()时,event对象仍然是window对象的属性(像DOM0方式那样),只是出于方便也将其作为参数传入。
如果是使用HTML属性方式指定的事件处理程序,则event对象同样可以通过变量event访问(与DOM模型一样)。
所有IE事件对象都会包含下表所列的公共属性和方法:
image.png
由于事件处理程序的作用域取决于指定它的方式,因此this值并不总是等于事件目标。
为此,更好的方式是使用事件对象的srcElement属性代替this。
returnValue属性等价于DOM的preventDefault()方法,都是用于取消给定事件默认的行为。
但在这里要把returnValue设置为false才是阻止默认动作。
cancelBubble属性与DOMstopPropagation()方法用途一样,都可以阻止事件冒泡。
因为IE8及更早版本不支持捕获阶段,所以只会取消冒泡。
stopPropagation()则既取消捕获也取消冒泡。

17.3.3 跨浏览器事件

DOM事件对象中包含IE事件对象的所有信息和能力,只是形式不同。这些共性可让两种事件模型之间的映射成为可能。

  1. var EventUtil = {
  2. addHandler: function(element, type, handler) {
  3. // 省略之前的代码
  4. },
  5. getEvent: function(event) {
  6. return event ? event : window.event;
  7. },
  8. getTarget: function(event) {
  9. return event.target || event.srcElement;
  10. },
  11. preventDefault: function(event) {
  12. if (event.preventDefault) {
  13. event.preventDefault();
  14. } else {
  15. event.returnValue = false;
  16. }
  17. },
  18. removeHandler: function(element, type, handler) {
  19. // 省略之前的代码
  20. },
  21. stopPropagation: function(event) {
  22. if (event.stopPropagation) {
  23. event.stopPropagation();
  24. } else {
  25. event.cancleBuble = true;
  26. }
  27. }
  28. };

17.5 内存与性能

在JavaScript中,页面中事件处理程序的数量与页面整体性能直接相关。
原因有二:
首先,每个函数都是对象,都占用内存空间,对象越多,性能越差。
其次,为指定事件处理程序所需访问DOM的次数会先期造成整个页面交互的延迟。
只要在使用事件处理程序时多注意一些方法,就可以改善页面性能。

17.5.1 事件委托

“过多事件处理程序”的解决方案是使用事件委托。
事件委托利用事件冒泡,可以只使用一个事件处理程序来管理一种类型的事件。
例如,click事件冒泡到document。这意味着可以为整个页面指定一个onclick事件处理程序,而不用为每个可点击元素分别指定事件处理程序。
假设出现大片雷同的只为指定事件处理程序的代码。使用事件委托,只要给所有元素共同的祖先节点添加一个事件处理程序,就可以解决问题。只要检查event对象的id属性就可以确定,然后再执行相应的操作即可。
这样的操作不会导致先期延迟,因为只访问了一个DOM元素和添加了一个事件处理程序。结果对用户来说没有区别,但这种方式占用内存更少。
所有使用按钮的事件(大多数鼠标事件和键盘事件)都适用于这个解决方案。
只要可行,就应该考虑只给document添加一个事件处理程序,通过它处理页面中所有某种类型的事件。
相对于之前的技术,事件委托具有如下优点:
❑ document对象随时可用,任何时候都可以给它添加事件处理程序(不用等待DOMContentLoaded或load事件)。这意味着只要页面渲染出可点击的元素,就可以无延迟地起作用。
❑ 节省花在设置页面事件处理程序上的时间。只指定一个事件处理程序既可以节省DOM引用,也可以节省时间。
❑ 减少整个页面所需的内存,提升整体性能。
最适合使用事件委托的事件包括:click、mousedown、mouseup、keydown和keypress。

17.5.2 删除事件处理程序

很多Web应用性能不佳都是由于无用的事件处理程序长驻内存导致的。
导致这个问题的原因主要有两个:
第一个是删除带有事件处理程序的元素。
比如通过真正的DOM方法removeChild()或replaceChild()删除节点。
最常见的还是使用innerHTML整体替换页面的某一部分。这时,被innerHTML删除的元素上如果有事件处理程序,就不会被垃圾收集程序正常清理。
如果知道某个元素会被删除,那么最好在删除它之前手工删除它的事件处理程序。
注:在事件处理程序中删除按钮会阻止事件冒泡。只有事件目标仍然存在于文档中时,事件才会冒泡。
注:事件委托也有助于解决这种问题。如果提前知道页面某一部分会被使用innerHTML删除,就不要直接给该部分中的元素添加事件处理程序了。把事件处理程序添加到更高层级的节点上同样可以处理该区域的事件。
另一个可能导致内存中残留引用的问题是页面卸载。
如果在页面卸载后,事件处理程序没有被清理,则它们仍然会残留在内存中。
之后,浏览器每次加载和卸载页面(比如通过前进、后退或刷新),内存中残留对象的数量都会增加,这是因为事件处理程序不会被回收。
一般来说,最好在onunload事件处理程序中,趁页面尚未卸载,先删除所有事件处理程序。这时候也能体现使用事件委托的优势,因为事件处理程序很少,所以很容易记住要删除哪些。
记住一点:onload事件处理程序中做了什么,最好在onunload事件处理程序中恢复。

17.6 模拟事件

17.6.1 DOM事件模拟

document.createEvent()方法创建一个event对象。接收一个参数,此参数是一个表示要创建事件类型的字符串。在DOM2中,所有这些字符串都是英文复数形式
但在DOM3中,又把它们改成了英文单数形式。
可用的字符串值是以下值之一:
❑ “UIEvents”(DOM3中是”UIEvent”):通用用户界面事件(鼠标事件和键盘事件都继承自这个事件)。
❑ “MouseEvents”(DOM3中是”MouseEvent”):通用鼠标事件。
❑ “HTMLEvents”(DOM3中没有):通用HTML事件(HTML事件已经分散到了其他事件大类中)。
注:键盘事件是后来在DOM3 Events中增加的。
事件模拟的最后一步是触发事件。使用dispatchEvent()方法,这个方法存在于所有支持事件的DOM节点之上。接收一个参数,即表示要触发事件的event对象。
调用dispatchEvent()方法之后,事件就“转正”了,接着便冒泡并触发事件处理程序执行。

1.模拟鼠标事件

模拟鼠标事件需先创建一个新的鼠标event对象,然后再使用必要的信息对其进行初始化。
创建鼠标event对象,可调用createEvent()方法并传入”MouseEvents”参数。会返回一个event对象,这个对象有一个initMouseEvent()方法,用于为新对象指定鼠标的特定信息。
initMouseEvent()方法接收15个参数,分别对应鼠标事件会暴露的属性。这些参数与鼠标事件的event对象属性是一一对应的。

2.模拟键盘事件

注意:DOM3 Events中定义的键盘事件,与DOM2 Events草案最初定义的键盘事件差别很大。
在DOM3中创建键盘事件的方式是:给createEvent()方法传入参数”KeyboardEvent”。返回一个event对象,这个对象有一个initKeyboardEvent()方法;
Firefox允许给createEvent()传入”KeyEvents”来创建键盘事件。这时候返回的event对象包含的方法叫initKeyEvent();
键盘事件也可通过调用dispatchEvent()并传入event对象来触发。

3.模拟其他事件

鼠标事件和键盘事件是浏览器中最常见的模拟对象。但是,有时可能也需要模拟HTML事件。
模拟HTML事件要调用createEvent()方法并传入”HTMLEvents”,再使用返回对象的initEvent()方法初始化。
注:HTML事件在浏览器中很少使用,因为它们用处有限。

4.自定义DOM事件

DOM3增加了自定义事件的类型。
自定义事件不会触发原生DOM事件,但可以让开发者定义自己的事件。
要创建自定义事件,需调用createEvent(“CustomEvent”)。返回的对象包含initCustomEvent()方法,该方法接收以下4个参数:
❑ type(字符串):要触发的事件类型,如”myevent”。
❑ bubbles(布尔值):表示事件是否冒泡。
❑ cancelable(布尔值):表示事件是否可以取消。
❑ detail(对象):任意值。作为event对象的detail属性。
自定义事件可以像其他事件一样在DOM中派发。

17.6.2 IE事件模拟

首先,要使用document对象的createEventObject()方法创建event对象。不接收参数,返回一个通用event对象。然后,可以手工给返回的对象指定希望该对象具备的所有属性。(没有初始化方法。)
最后一步,在事件目标上调用fireEvent()方法,接收两个参数:事件处理程序的名字和event对象。
调用fireEvent()时,srcElement和type属性会自动指派到event对象(其他所有属性必须手工指定)。
这意味着IE支持的所有事件都可以通过相同的方式来模拟。