事件Event(下)

前端开发
2018年12月14日
597

复合事件

复合事件(composition event)是DOM3级事件中新添加的一类事件,用于处理IME的输入序列。IME(Input Method Editor,输入法编辑器)可以让用户输入在物理键盘上找不到的字符。IME通常需要同时按住多个键,但最终只输入一个字符。

有以下三种复合事件:

  • compositionstart:在IME的文本复合系统打开时触发。
  • compositionupdate:在向输入字段中插入新字符时触发。
  • compositionend:在IME文本复合系统关闭时触发,表示返回正常键盘输入状态。

复合事件与文本事件有很多方面很相似。在触发复合事件时,目标是接收文本的输入字段。但它比文本事件的事件对象多一个属性data,其中包含以下几个值中的一个:

  • 如果在compositionstart事件发生时访问,包含正在编辑的文本(例如,已经选中的需要马上替换的文本);
  • 如果在compositionupdate事件发生时访问,包含正插入的新字符;
  • 如果在compositionend事件发生时访问,包含此次输入会话中插入的所有字符。

变动事件

DOM2级的变动(mutation)事件能在DOM中的某一部分发生变化时给出提示。变动事件是为XML或HTML DOM设计的,并特定于某种语言。DOM2级定义了如下变动事件:

  • DOMSubtreeModified:在DOM结构中发生变化时触发。这个事件在其他任何事件触发后都会触发。
  • DOMNodeInserted:在一个节点作为子节点被插入到另一个节点中时触发。
  • DOMNodeRemoved:在节点从其父节点中被移除时触发。
  • DOMNodeInsertedIntoDocument:在一个节点这被插入文档之后触发。这个事件在DOMNodeInserted之后触发。
  • DOMNodeRemovedFromDocument:在一个节点从文档中被移除之前触发。这个事件在DOMNodeRemoved之后触发。
  • DOMAttrModifed:在特性被修改之后触发。
  • DOMCharacterDataModified:在文本节点的值发生变化时触发。

使用下列代码可以检测浏览器是否支持变动事件:

js
var isSupported = document.implementation.hasFeature('MutationEvents', '2.0');

IE8及更早版本不支持任何变动事件。

|事件|Opera9+|Firefox3+|Safari3+及Chrome|IE9+|
|-|-|-|-|-|-|
|DOMSubtreeModified|-|支持|支持|支持|
|DOMNodeInserted|支持|支持|支持|支持|
|DOMNodeRemoved|支持|支持|支持|支持|

删除节点

在使用removeChild()replaceChild()从DOM中删除节点时,首先会触发DOMNodeRemoved事件。这个事件的目标是被删除的节点,而event.relatedNode属性中包含着对目标节点父节点的引用。在这个事件触发时,节点尚未从其父节点删除,因此其parentNode属性仍然指向父节点(与event.relatedNode相同)。这个事件会冒泡,因而可以在DOM的任何层次上面处理它。

如果被移除的节点包含子节点,那么在其所有子节点以及这个被移除的节点上会相继触发DOMNodeRemovedFromDocument事件。但这个事件不会冒泡,所有只有直接指定给其中一个子节点的事件处理程序才会被调用。这个事件的目标是相应的子节点或者那个被移除的节点,除此之外,event对象中不包含其他信息。

紧随其后触发的是DOMSubtreeModified事件。这个事件的目标是被移除节点的父节点;此时的event对象也不会提供与事件相关的其他信息。

以下面的HTML为例:

html
<!DOCTYPE html> <html> <head> <title>Node Removal Events Example</title> </head> <body> <ul id="myList"> <li>Item 1</li> <li>Item 2</li> <li>Item 3</li> </ul> </body> </html>

在这个例子中,我们假设要移除<ul>元素。此时,就会依次触发以下事件。

plain-text
1. 在ul元素上触发DOMNodeRemoved事件。relatedNode属性等于document.body。 2. 在ul元素上触发DOMNodeRemovedFromDocument事件。 3. 在身为ul元素子节点的每个li元素及文本节点上触发DOMNodeRemovedFromDocument事件 4. 在document.body上触发DOMSubtreeModified事件,因为ul是document.body的直接子元素。

使用下面的代码可以验证事件发生的顺序:

js
EventUtil.addHandler(window, 'load', function (event) { var list = document.getElementById('myList'); EventUtil.addHandler(document, 'DOMSubtreeModified', function (event) { alert(event.type); alert(event.target); }); EventUtil.addHandler(document, 'DOMNodeRemoved', function (event) { alert(event.type); alert(event.target); alert(event.relatedNode); }); EventUtil.addHandler(list.firstChild, 'DOMNodeRemovedFromDocument', function (event) { alert(event.type); alert(event.target); }); list.parentNode.removeChild(list); });

插入节点

在使用appendChild()replaceChild()insertBefore()向DOM中插入节点时,首先会触发DOMNodeInserted事件。这下事件的目标是被插入的节点,而event.relatedNode属性中包含一个对父节点的引用。在这个事件触发时,节点已经被插入到新的父节点中。这个事件是冒泡的,因此可以在DOM的各层次上处理它。

紧接着,会在新插入的节点上面触发DOMNodeInsertedIntoDocument事件。这个事件不冒泡,因此必须在插入节点之前为它添加这个事件处理程序。这个事件的目标是被插入的节点,除此之外event对象不包含其他信息。

最后一个触发的事件是DOMSubtreeModified,触发于新插入节点的父节点。

仍以前面的HTMl文档为例。

js
EventUtil.addHandler(window, 'load', function (event) { var list = document.getElementById('myList'); var item = document.createElement('li'); itam.appendChild(document.createTextNode('Item 4')); EventUtil.addHandler(document, 'DOMSubtreeModified', function (event) { alert(event.type); alert(event.target); }); EventUtil.addHandler(document, 'DOMNodeInserted', function (event) { alert(event.type); alert(event.target); alert(event.relatedNode); }); EventUtil.addHandler(item, 'DOMNodeInsertedIntoDocument', function (event) { alert(event.type); alert(event.target); }); list.appendChild(item); });

HTML5事件

contextmenu事件

Win95在PC中引入一上下文菜单的概念,即通过单击鼠标右键可以调出上下文菜单。不久,这个概念也被引入了Web领域。

contextmenu事件,用以表示何时应该显示上下文菜单,以便开发人员取消默认的上下文菜单而提供自定义的菜单。

由于contextmenu事件是冒泡的,因此可以为document指定一个事件处理程序,用以处理页面中发生的所有此类事件。这个事件的目标是发生用户操作的元素。在所有浏览器中都可以取消这个事件:在兼容DOM的浏览器中,使用event.preventDefault();在IE中,将event.returnValue的值设置为false。因为contextmenu事件属于鼠标事件,所以其事件对象中包含与光标位置有关的所有属性。通常使用contextmenu事件来显示自定义的上下文菜单,而使用onclick事件处理程序来隐藏该菜单。以下面的HTML为例。

html
<!DOCTYPE html> <html> <head> <title>ContextMenu Event Example</title> </head> <body> <div id="myDiv">右击或按住ctrl单击我可以打开定制的上下文菜单,点击其他地方可以打开默认的上下文菜单</div> <ul id="myMenu" style="position: absolute; visibility: hidden; background-color: silver;"> <li><a href="http://www.humandetail.com">Humandetail Site</a></li> <li><a href="https://www.baidu.com">百度一下,你就知道</a></li> <li><a href="https://www.qq.com">企鹅帝国</a></li> </ul> </body> </html>
js
EventUtil.addHandler(window, 'load', function (event) { var div = document.getElementById('myDiv'); EventUtil.addHandler(div, 'contextmenu', function (event) { event = EventUtil.getEvent(event); EventUtil.preventDefault(event); var menu = document.getElementById('myMenu'); menu.style.left = event.clientX + 'px'; menu.style.top = event.clientY + 'px'; menu.style.visibility = 'visible'; }); EventUtil.addHandler(document, 'click', function (event) { document.getElementById('myMenu').style.visibility = 'hidden'; }); });

支持contextmenu事件的浏览器有IE、Firefox、Safari、Chrome和Opera11+。

beforeunload事件

之所有有发生在window对象上的beforeunload事件,是为了让开发人员有可能在页面卸载之前阻止这一操作。这个事件会在浏览器卸载页面之前触发,可以通过它来取消卸载并继承使用原有页面。

但是,不能彻底取消这个事件,因为那就相当于让用户无法离开当前页面了。为此,这个事件的意图是将控制权交给用户。显示的消息会告诉用户页面将被卸载,询问用户是否真的要关闭页面。

为了显示询问对话框,必须将event.returnValue的值设置为要显示给用户的字符串(对Ie及Firefox而言),同时作为函数的值返回(对Safari和Chrome而言)。

js
EventUtil.addHandler(window, 'beforeunload', function (event) { event = EventUtil.getEvent(event); var msg = '是否要关闭当前页面?'; event.returnValue = msg; return msg; });

IE、Firefox、Safari和Chorme都支持beforeunload事件,也都会弹出询问框。Opera11及之前版本不支持。

DOMContentLoaded事件

window的load事件会在页面中的一切都加载完毕时触发。而DOMContentLoaded事件则在形成完整的DOM树之后就会触发。与load事件不同,DOMContentLoaded支持在页面下载的早期添加事件处理程序,这也就意味着用户能够尽早地与页面进行交互。

js
EventUtil.addHandler(document, 'DOMContentLoaded', function (event) { alert('Content Loaded'); });

DOMContentLoaded的事件对象不会提供任何额外的信息(其target属性是document)。

IE9+、Firefox、Chrome、Safari3.1+和Opera9+都支持DOMContentLoaded事件。这个事件始终在load事件之前触发。

readystatechange事件

IE为DOM文档中的某些部分提供了readystatechange事件。这个事件的目的是指代与文档或元素的加载状态有关的信息,但这个事件的行为有时候也很难预料。支持readystatechange事件的每个对象都有一个readyState属性,可能包含下列5个值中的一个:

  • uninitialized(未初始化):对象存在,但尚未初始化。
  • loading(正在加载):正在加载数据。
  • loaded(加载完毕):数据加载完毕
  • interactive(交互):可以操作对象了,但还没有完全加载。
  • complete(完成):对象加载完毕。

支持readystatechange事件的浏览器有IE、Firefox4+和Opera。

pageshow和pagehide事件

Firefox和Opera中有一个特性,名叫“往返缓存”(back-forward cache,或bfcache),可以在用户使用浏览器“后退”和“前进”按钮时加快页面的转换速度。这个缓存中不仅保存着页面数据,还保存着DOM和JavaScript的状态;实际上是将整个页面都保存在内存里。如果页面位于bfcache中,那么再次打开该页面不会触发load事件。为了更形象地说明bfcache的行为,Firefox还是提供了一些新事件。

第一个事件就是pageshow,这个事件在页面显示时触发,无论该页面是否来自bfcache。在重新加载页面中,pageshow会在load事件后触发;而对于bfcache中的页面,pageshow会在页面完全恢复的那一刻触发。需要注意的是,虽然这个事件的目标是document,但必须将其事件处理程序添加到window。

js
(function () { var showCount = 0; EventUtil.addHandler(window, 'load', function () { alert('Loaded Fired'); }); EventUtil.addHandler(window, 'pageshow', function () { showCount++; alert(showCount); }); })

除了通常的属性之外,pageshow事件的event对象还包含一个名叫persisted的布尔值属性。如果页面被保存在bfcache中,则这个属性值为true,否则为false。

pageshow事件对应的是pagehide事件,该事件会在浏览器卸载页面的时候触发,而且是在unload事件之前触发。

支持这两个事件的浏览器有Firefox、Safari5+、Chrome和Opera。IE9及之前版本不支持。

hashchange事件

HTML5新增了hashchange事件,以便在URL参数列表(及URL中“#”号后面的所有字符串)发生变化时通知开发人员。之所以新增这个事件,是因为在AJAX应用中,开发人员经常要利用URL参数列表来保存状态或导航信息。

必须要把hashchange事件处理程序添加给window对象,然后URL参数列表只要变化就会调用它。此时的event对象应该额外包含两个属性:oldURLnewURL

js
EventUtil.addHandler(window, 'hashchange', function (event) { alert(event.oldURL, event.newURL); });

支持hashchange事件的浏览器有IE8+、Firefox3.6+、Safari5+、Chrome和Opera10.6+。在这些浏览器中,只有Firefox6+、Chrome和Opera支持oldURLnewURL属性。为此,最好使用location对象来确定当前的参数列表。

js
EventUtil.addHandler(window, 'hashchange', function (event) { alert('Current hash: ' + location.hash); });

使用以下代码可以检测浏览器是否支持hashchange事件

js
var isSupported = ('onhashchange' in window) && (document.documentMode === undefined || document.documentMode > 7);

设备事件

orientationchange事件

苹果公司为移动Safari中添加了orientationchange事件,以便开发人员能够确定用户何时将设备由横向查看模式切换为纵向查看模式。移动Safari的window.orientation属性中可能包含3个值:0表示肖像模式(即正常的纵向模式),90表示向左旋转的横向模式(HOME键在右侧),-90表示向右旋转的横向模式(HOME键在左侧)。

只要用户改变了设备的查看模式,就会触发orientationchange事件。此时的event对象不包含任何有价值的信息,因为唯一相关的信息可以通过window.orientation属性访问到。

js
EventUtil.addHandler(window, 'load', function (event) { var div = document.getElementById('myDiv'); div.innerHTML = 'Current orientation is ' + window.orientation; EventUtil.addHandler(window, 'orientationchange', function (event) { div.innerHTML = 'Current orientation is ' + window.orientation; }); });

所有IOS设备都支持orientationchange事件window.orientation属性

MozOrientation事件

Firefox3.6为检测设备的方向引入了一个名为MozOrientation的新事件。(前缀Moz表示这是特定于浏览器开发商的事件,不是标准事件)。这个事件与IOS中的orientationchange事件不同,IOS中的事件只能提供一个平面方向的变化。由于MozOrientation事件是在window对象上触发的,可以使用以下代码来处理:

js
EventUtil.addHandler(window, 'MozOrientation', function (event) { // 响应事件 });

此时的event对象包含三个属性:x、y和z。这几个属性的值都介于1到-1之间,表示不同坐标轴上的方向。在静止状态下,x值为0,y值为0,z值为1(表示设备处于竖直状态)。如果设备向右倾斜,x值会减小;反之,向左倾斜,x值会增大。类似地,如果设备向远离用户的方向倾斜,y值减小,向接近用户的方向倾斜,y值增加。z轴检测垂直加速度,1表示静止不动,在设备移动时值会减小。(失重状态值为0)。

js
EventUtil.addHandler(window, 'MozOrientation', function (event) { var output = document.getElementById('output'); output.innerHTML = 'X: ' + event.x + '; Y: ' + event.y + '; Z: ' + event.z + '.<br>'; });

只有带加速计的设备才支持MozOrientation事件,包括Macbook、Lenovo Thinkpad、Windows Mobile和Android设备。注意,这是一个实验性API,将来可能会变。

deviceorientation事件

本质上DeviceOrientation Event规范定义的deviceorientation事件MozOrientation事件类似。它也是在加速计检测到设备方向变化时在window对象上触发,而且具有与MozOrientation事件相同的支持限制。不过,deviceorientation事件的意图是告诉开发人员设备在空间中朝向哪儿,而不是如何移动。

设备在三维空间中是靠x、y和z轴来定位的。当设备静止放在水平表面上时,这三个值都是0。x轴方向是从左往右,y轴方向是从下往上,z轴方向是从后往前。

触发devicorientation事件时,事件对象中包含着每个轴相对于设置静止状态下发生变化的信息。事件对象包含以下5个属性:

  • alpha:在围绕z轴旋转时(即左右旋转),y轴的度数差;是一个介于0到360之间的浮点数。
  • beta:在围绕x轴旋转时(即前后旋转),z轴的度数差;是一个介于-180到180之间的浮点数。
  • gamma:在围绕y轴旋转时(即扭转设备),z轴的度数差;是一个介于-90到90之间的浮点数。
  • absolute:布尔值,表示设备是否返回一个绝对值。
  • compassCalibrated:布尔值,表示设备的指南针是否校准过。
js
EventUtil.addHandler(window, 'deviceorientation', function (event) { var output = document.getElementById('output'); output.innerHTML = 'Alpha: ' + event.alpha + '; Beta: ' + event.beta + '; Gamma: ' + event.gamma + '.<br>'; });

devicemotion事件

devicemotion事件是要告诉开发人员设备什么时候移动,而不仅仅是设备方向如何改变。例如,通过devicemotion能够检测到设备是不是正在往下掉,或者是不是被走着的人拿在手里。

触发devicemotion事件时,事件对象包含以下属性。

  • acceleration:一个包含x、y和z属性的对象,在不考虑重力的情况下,告诉你在每个方向的加速度。
  • accelerationIncludingGravity:一个包含x、y和z属性的对象,在考虑z轴自然重力加速度的情况下,告诉你在每个方向的加速度。
  • interval:以毫秒表示的时间值,必须在另一个devicemotion事件触发前传入。这个值在每个事件中应该是一个常量。
  • rotationRage:一个包含表示方向的alphabetagamma属性的对象。

触摸与手势事件

触发事件

触摸事件会在用户手指放在屏幕上面时、在屏幕上滑动时或从屏幕上移开时触发。具体来说,有以下几个触摸事件:

  • touchstart:当手指触摸屏幕时触发;即使已经有一个手指放在了屏幕上也会触发。
  • touchmove:当手指在屏幕上滑动时连续地触发。在这个事件发生期间,调用preventDefault()可以阻止滚动。
  • touchend:当手指从屏幕上移开时触发。
  • touchcancel:当系统停止跟踪触摸时触发。

上面这几个事件都会冒泡,也都可以取消。虽然这些触摸事件没有在DOM规范中定义,但它们却是以兼容DOM的方式实现的。因此,每个触摸事件的event对象都提供了在鼠标事件中的常见属性:bubbles、cancelable、view、clientX、clientY、screenX、screenY、detail、shiftKey、ctrlKey和metaKey

除了常见的DOM属性外,触摸事件还包含下列三个用于跟踪触摸的属性。

  • touches:表示当前跟踪的触摸操作的Touch对象的数组。
  • targetTouchs:特写于事件目标的Touch对象的数组。
  • changeTouches:表示上次触摸以来发生了什么改变的Touch对象的数组。

每个Touch对象包含下列属性:

  • clientX:触摸目标在视口中的x坐标。
  • clientY:y坐标。
  • identifier:触摸的唯一标识ID。
  • pageX:触摸目标在页面中的x坐标。
  • pageY:y坐标。
  • screenX:触摸目标在屏幕中的x坐标。
  • screenY:y坐标。
  • target:触摸的DOM节点目标。

使用这些属性可以跟踪用户对屏幕触摸操作:

js
function handleTouchEvent (event) { // 只跟踪一次触摸 if (event.touches.length === 1) { var output = document.getElementById('output'); switch (event.type) { case 'touchstart': output.innerHTML = 'Touch started (' + event.touches[0].clientX + ',' + event.touches[0].clientY + ').'; break; case 'touchend': output.innerHTML = '<br>Touch ended (' + event.changedTouches[0].clientX + ',' + event.changedTouches[0].clientY + ').'; break; case 'touchmove': event.preventDefault(); // 阻止滚动 output.innerHTML = '<br>Touch moved (' + event.changedTouches[0].clientX + ',' + event.changedTouches[0].clientY + ').'; break; } } } EventUtil.addHandler(document, 'touchstart', handleTouchEvent); EventUtil.addHandler(document, 'touchmove', handleTouchEvent); EventUtil.addHandler(document, 'touchend', handleTouchEvent);

这些事件会在文档的所有元素上面触发,因而可以分别操作页面的不同部分。在触摸屏幕上的元素时,这些事件(包括鼠标事件)发生的顺序如下:

plain-text
1. touchstart 2. mouseover 3. mousemove(一次) 4. mousedown 5. mouseup 6. click 7. touchend

支持触摸事件的浏览器包括IOS版Safari、Android版WebKit、bada版Dolfin、OS6+中的BlackBerry WebKit、Opera Mobile 10.1+和LG专有的OS中的Phantom浏览器。目前只有IOS版Safari支持多点触摸。桌面版Firefox6+和Chrome也支持触摸事件。

手势事件

当两个手指触摸屏幕时就会产生手势,手势通常会改变显示项大小,或者旋转显示项。有三个手势事件:

  • gesturestart:当一个手指已经按在屏幕上而另一个手指又触摸屏幕时触发。
  • gesturechange:当触摸屏幕的任何一个手指的位置发生变化时触发。
  • gestureend:当任何一个手指从屏幕上面移开时触发。

只有两个手指都触摸到事件的接收容器时才会触发这些事件。在一个元素上设置事件处理程序,意味着两个手指必须同时位于该元素的范围之内,才能触发手势事件(这个元素就是目标)。由于这些事件冒泡,所以将事件处理程序放在文档上也可以处理所有手势事件。此时,事件的目标就是两个手指都位于其范围内的那个元素。

触摸事件和手势事件之间存在某种关系。当一个手指放在屏幕时,会触发touchstart事件。如果另一个手指又放在屏幕上,则会先触发gesturestart事件,随后触发基于该手指的touchstart事件。如果一个或两个手指在屏幕上滑动,将会触发gesturechange事件。但只要有一个手指移开,就会触发gestureend事件,紧接着又会触发基于该手指的touchend事件

与触摸事件一样,每个手势事件的event对象都包含着标准的鼠标事件属性。此外,还包含两个额外的属性:rotationscale。其中,rotation属性表示手指变化引起的旋转角度,负值表示逆时针旋转,正值表示顺时针旋转(该值从0开始)。而scale属性表示两个手指间距离变化的情况(例如向内收缩会缩短距离),这个值从1开始,并随距离拉大而增长,拉小而减小。

下面是使用手势事件的一个示例:

js
function handleGestureEvent (event) { var output = document.getElementById('output'); switch (event.type) { case 'gesturestart': output.innerHTML = 'Gestrue started (rotation=' + event.rotation + ',scale=' + event.scale + ').'; break; case 'getstureend': output.innerHTML = '<br>Gestrue ended (rotation=' + event.rotation + ',scale=' + event.scale + '.'; break; case 'getsturechange': output.innerHTML = '<br>Gestrue changed (rotation=' + event.rotation + ',scale=' + event.scale + ').'; break; } } document.addEventListener('gesturestart', handleGestureEvent, false); document.addEventListener('gestureend', handleGestureEvent, false); document.addEventListener('gesturechange', handleGestureEvent, false);

内存和性能

事件委托

对“事件处理程序过多”问题的解决方案就是事件委托。事件委托利用了事件冒泡,只指定一个事件处理程序,就可以管理某一类型的所有事件。例如,click事件会一直冒泡到document层次。也就是说,我们可以为整个页面指定一个onclick事件处理程序,而不必给每个可单击的元素分别添加事件处理程序。

以下面的HTML代码为例:

html
<ul id="myLinks"> <li id="goSomewhere">Go Somewhere</li> <li id="doSomething">Do Something</li> <li id="sayHi">Say Hi</li> </ul>

其中包含3个被单击后执行操作的列表项。按照传统的做法,需要像下面这样为它们添加3个事件处理程序:

js
var item1 = document.getElementById('goSomewhere'); var item2 = document.getElementById('doSomething'); var item3 = document.getElementById('sayHi'); EventUtil.addHandler(item1, 'click', function (event) { location.href = 'http://www.humandetail.com'; }); EventUtil.addHandler(item2, 'click', function (event) { document.title = '我改变了文档的标题' }); EventUtil.addHandler(item3, 'click', function (event) { alert('Hi'); });

如果在一个复杂的Web应用程序中,对所有可单击的元素都采用这种方式,那么结果就会有数不清的代码用于添加事件处理程序。此时,可以利用事件委托技术来解决这个问题:

js
var list = document.getElementById('myLinks'); EventUtil.addHandler(list, 'click', function (event) { event = EventUtil.getEvent(event); var target = EventUtil.getTarget(event); switch (target.id) { case 'goSomewhere': location.href = 'http://www.humandetail.com'; break; case 'doSomething': document.title = '我改变了文档的标题' break; case 'sayHi': alert('Hi'); break; } });

这样做与采取传统的做法相比较有如下优点:

  • document对象很快就可以访问,而且可以在页面生命周期的任何时间点上为它添加事件处理程序(无需等待DOMContentLoaded或load事件)。换句话说,只要可单击的元素呈现在页面上,就可以立即具备适当的功能。
  • 在页面中设置事件处理程序所需的时间更少。只添加一个事件处理程序所需的DOM引用更少,所花的时间也更少。
  • 整个页面在占用的内存空间更少,能够提升整体性能。

最适合采用事件委托技术的事件包括click、mousedown、mouseup、keydown、keyup和keypress。虽然mouseovermouseout事件也冒泡,但要适当处理它们并不容易,而且经常需要计算元素的位置。

移除事件处理程序

每当将事件处理程序指定给元素时,运行中的浏览器代码与支持页面交互的JavaScript代码之间就会建立一个连接。这种连接越多,页面执行起来就越慢。如前所述,可以采用事件委托技术,限制建立的连接数量。另外,在不需要的时候移除事件处理程序,也是解决这个问题的一种方案。内存中留有那些过时不用的“空事件处理程序”(dangling event handler),也是造成Web应用程序内存与性能问题的主要原因。

在两种情况下,可能会造成上述问题。第一种情况就是从文档中移除带有事件处理程序的元素时,可能是通过纯粹的DOM操作,例如使用removeChild()replaceChild()方法,但更多地是发生在使用innerHTML替换页面中某一部分的时候。如果带有事件处理程序的元素被innerHTML删除了,那么原来添加到元素中的事件处理程序极有可能无法被当作垃圾回收。

看下面的例子:

html
<div id="myDiv"> <input type="button" value="Click Me" id="myBtn"> </div> <script> var btn = document.getElementById('myBtn'); btn.onclick = function () { // 先执行某些操作 document.getElementById('myDiv').innerHTML = 'Processing...'; } </script>

<div>元素上设置innerHTML把按钮给移走了,但事件处理程序仍然与按钮保持着引用关系。有的浏览器(尤其是IE)在这种情况下不会作出恰当的处理,它们很有可能会将元素和对事件处理程序的引用都保存在内存中。如果你知道某个元素即将被移除,那么最好手工移除事件处理程序。

html
<div id="myDiv"> <input type="button" value="Click Me" id="myBtn"> </div> <script> var btn = document.getElementById('myBtn'); btn.onclick = function () { // 先执行某些操作 btn.onclick = null; // 先移除事件处理程序 document.getElementById('myDiv').innerHTML = 'Processing...'; } </script>

导致“空事件处理程序”的另一种情况就是卸载页面的时候。

一般来说,最好是在页面卸载之前,先通过onunload事件处理程序移除所有事件处理程序。在此,事件委托技术再次表现出它的优势——需要跟踪的事件处理程序越少,移除它们就越容易。对于这种类似撤销的操作,我们可以把它想象成:只要通过onload事件处理程序添加的东西,最后都需要通过onunload事件处理程序将它们移除

模拟事件

事件,就是网页在某个特别值得关注的瞬间。事件经常由用户操作或通过其他浏览器功能来触发。但很少有人知道,也可以使用JavaScript在任意时刻来触发特定的事件,而此时的事件就如同浏览器创建的事件一样。也就是说,这些事件该冒泡的还会冒泡,而且照样能够导致浏览器执行已经指定的处理它们的事件处理程序。在测试Web应用程序,模拟触发事件是一种极其有用的技术。DOM2级规范为此规定了横扫特定事件的方式,IE9、Opera、Firefox、Chrome和Safari都支持这种方式。IE有它自己模拟事件的方式。

DOM中的事件模拟

可以在document对象上使用createEvent()方法创建event对象。这个方法接收一个参数,即表示要创建的事件类型的字符串。在DOM2级中,所有这些字符串都使用英文复数形式,而在DOM3级中都变成了单数。这个字符串可以是下列几个字符串之一:

  • UIEvents:一般化UI事件。鼠标事件和键盘事件都继承自UI事件。DOM3级中是UIEvent
  • MouseEvents:一般化的鼠标事件。DOM3级中是MouseEvent
  • MutationEvents:一般化的DOM变动事件。DOM3级中是MutationEvent
  • HTMLEvents:一般化的HTML事件。没有对应的DOM3级事件。

在创建了event对象之后,还需要使用与事件有关的信息对其进行初始化。每种类型的event对象都有一个特殊的方法,为它传入适当的数据就可以初始该event对象。不同类型的这个方法的名字也不同,具体要取决于createEvent()中使用的参数。

模拟事件的最后一步就是触发事件。这一步需要调用dispatchEvent()方法,所有支持事件的DOM节点都支持这个方法。调用dispatchEvent()方法时,需要传入一个参数,即表示要触发事件的event对象。触发事件之后,该事件就跻身“官方事件”之列了,因而能够照样冒泡并引发相应事件处理程序的执行。

模拟鼠标事件

创建新的鼠标事件对象并为其指定必要的信息,就可以模拟鼠标事件。创建鼠标事件对象的方法是为createEvent()传入字符串"MouseEvents"。返回的对象有一个名为initMouseEvent()的方法,用于指定与该鼠标事件有关的信息。这个方法接收15个参数,分别与鼠标事件中每个典型属性一一对应;这些参数的含义如下:

  • type
  • bubbles
  • cancelable
  • view
  • detail
  • screenX
  • screenY
  • clientX
  • clientY
  • ctrlKey
  • altKey
  • shiftKey
  • metaKey
  • button
  • relatedTarget

其中,前4个参数对正确激发事件至关重要,因为浏览器要用到这些参数;而剩下的参数只有在事件处理程序中才会用到。当把event对象传给dispatchEvent()方法时,这个对象的target属性会自动设置。

js
var btn = document.getElementById('myBtn'); // 创建事件对象 var event = document.createEvent('MouseEvents'); // 初始化事件对象 event.initMouseEvent('click', true, true, document.defaultView, 0, 0, 0, 0, 0, false, false, false, false, 0, null); // 触发事件 btn.dispatchEvent(event);

模拟键盘事件

DOM3级规定,调用createEvent()并传入"KeyboardEvent"就可以创建一个键盘事件。返回的事件对象会包含一个initKeyEvent()方法,这个方法接收下列参数:

  • type
  • bubbles
  • cancelable
  • view
  • key
  • location
  • modifiers
  • repeat

由于DOM3级不提倡使用keypress事件,因此只能利用这种技术来模拟keydownkeyup事件。

js
var textbox = document.getElementById('myTextbox'), event; // 以DOM3级方式创建事件对象 if (document.implementation.hasFeature('KeyboardEvents', '3.0')) { event = document.createEvent('KeyboardEvent'); // 初始化事件对象 event.initKeyboardEvent('keydown', true, true, document.defaultView, 'a', 0, 'Shift', 0); } // 触发事件 textbox.dispatchEvent(event);

在Firefox中,调用createEvent()并传入"KeyEvents"就可以创建一个键盘事件。返回的事件对象会包含一个initKeyEvent()方法,这个方法接收下列10个参数:

  • type
  • bubbles
  • cancelable
  • view
  • ctrlKey
  • altKey
  • shiftKey
  • metaKey
  • keyCode
  • charCode
js
// 只适用于Firefox var textbox = document.getElementById('myTextbox'); // 创建事件对象 var event = document.createEvent('KeyEvents'); // 初始化事件对象 event.initKeyEvent('keypress', true, true, document.defaultView, false, false, false, false, 65, 65); // 触发事件 textbox.dispatchEvent(event);

在其他浏览器中,则需要创建一个通用事件,然后再向事件对象中添加键盘事件特有的信息。

js
var textbox = document.getElementById('myTextbox'); // 创建事件对象 var event = document.createEvent('Events'); // 初始化事件对象 event.initKeyEvent(type, bubbles, cancelable); event.view = document.defaultView; event.altKey = false; event.ctrlKey = false; event.shiftKey = false; event.metaKey = false; event.keyCode = 65; event.charCode = 65; // 触发事件 textbox.dispatchEvent(event);

以上代码首先创建了一个通用事件,然后调用initEvent()对其进行初始化,最后又为其添加了键盘事件的具体信息。在此必须要使用通用事件,而不能使用UI事件,因为UI事件不允许向event对象中再添加新属性(Safari除外)。像这样模拟事件虽然会触发键盘事件,但却不会向文本框中写入文本,这是由于无法精确模拟键盘事件造成的。

模拟其他事件

要模拟变动事件,可以使用createEvent('MutationEvents')创建一个包含initMutationEvevnt()方法的变动事件对象。这个方法接受的参数包括:type、bubbles、cancelable、relatedNode、preValue、newValue、attrName和attrChange。

js
var event = document.createEvent('MutationEvents'); event.initMutationEvent('DOMNodeInserted', true, false, someNode, '', '', '', 0); target.dispatchEvent(event);

要模拟HTML事件,需要通过createEvent('HTMLEvents')创建一个包含initEvent()方法的事件对象。

js
var event = document.createEvent('HTMLEvents'); event.initEvent('Focus', true, false); target.dispatchEvent(event);

自定义DOM事件

要创建自定义事件,可以调用createEvent('CustomEvent')创建一个包含initCustomEvent()方法的事件对象,这个方法接收4个参数:type、bubbles、cancelable和detail(对象,任意值,保存在event对象的detail属性中)。

js
var div = document.getElementById('myDiv'), event; if (document.implementation.hasFeature('CustomEvents', '3.0')) { event = document.createEvent('CustomEvents'); event.initCustomEvent('myevent', true, false, 'Hello world'); div.dispatchEvent(event); } EventUtil.addHandler(div, 'myevent', function (event) { alert(event.detail); })

支持自定义DOM事件的浏览器有IE9+和Firefox6+。

IE中的事件模拟

调用document.createEventObject()可以在IE中创建event对象。但与DOM方式不同的是,这个方法不接受参数,结果会返回一个通过的event对象。然后,必须手工为这个对象添加所有必要的信息。最后一步就是在目标上调用fireEvent()方法,这个方法接受两个参数:事件处理程序的名称和event对象。在调用fireEvent()时,会自动为event对象添加srcElementtype属性。

js
var btn = document.getElementById('myBtn'); // 创建事件对象 var event = document.createEventObject(); // 初始化事件对象 event.screenX = 100; event.screenY = 0; event.clientX = 0; event.clientY = 0; event.ctrlKey = false; event.altKey = false; event.shiftKey = false; event.button = 0; // 触发事件 btn.fireEvent('onclick', event);