事件Event(上)

前端开发
2018年12月14日
631

JavaScript与HTML之间的交互是通过事件实现的。事件,就是文档或浏览器窗口发生的一些特定的交互瞬间。可以使用侦听器(或处理程序)来预订事件,以便事件发生时执行相应的代码。这种在传统软件工程中被称为观察员模式的模型,支持页面的行为(JavaScript代码)与页面的外观(HTML和CSS代码)之间的松散耦合。

事件流

事件流描述的是从页面中接收事件的顺序。但有意思的是,IE和Netscape开发团队居然提出了差不多是完全相反的事件流概念。IE的事件流事件是事件冒泡流,而Netscape Communicator的事件流是事件捕获流

事件冒泡

IE的事件流叫做事件冒泡(event bubbling),即事件开始时由最具体的元素(文档中嵌套层次最深,也就是最底层的那个节点)接收,然后逐级向上传播到较为不具体的节点(文档)。以下面的HTML页面为例:

html
<!DOCTYPE html> <html> <head> <title>Event Bubbling Example</title> </head> <body> <div id="myDiv">Click Me</div> </body> </html>

如果你单击了页面中的<div>元素,那么这个click事件就会按照如下顺序传播:

plain-text
1. <div> 2. <body> 3. <html> 4. document

也就是说,click事件首先在div元素上发生,而这个元素就是我们单击的元素。然后,click事件沿DOM树向上传播,在每一级节点上都会发生,直到传播到document对象。

所有现代浏览器都支持事件冒泡,但在具体实现上还有一些差别。IE5.5及更早版本中的事件冒泡会跳过<html>元素。IE9、Firefox、Chrome和Safari则将事件一直冒泡到window对象

事件捕获

Netscape Communicator团队提出的另一种事件流叫事件捕获(event capturing)。事件捕获的思想是不太具体的节点应该更早接收事件,而最具体的节点应该最后接收到事件。事件捕获的用意在于事件到达预定目标之前捕获它。仍以上面的HTML页面为演示捕获的例子。那么单击<div>元素就会以下列顺序触发click事件:

plain-text
1. document 2. <html> 3. <body> 4. <div>

虽然事件捕获是Netscape Communicator唯一支持的事件流模型,但IE9、Safari、Chrome、Opera和Firefox目前也都支持这种事件流模式。尽管“DOM2级事件”规范要求事件应该从document对象开始传播,但这些浏览器都是从window对象开始捕获事件的。

由于老版本的浏览器不支持,因此很少有人使用事件捕获。建议大家放心地使用事件冒泡,在有特殊需要时再使用事件捕获。

DOM事件流

“DOM2级事件”规定的事件流包括三个阶段:事件捕获阶段、处于目标阶段和事件冒泡阶段。首先发生的是事件捕获,为截获事件提供了机会。然后是实际的目标接收到事件。最后一个阶段是冒泡阶段,可以在这个阶段对事件做出响应。

在DOM事件流中,实际的目标(<div>元素)在捕获阶段不会接收到事件。这意味着在捕获阶段,事件从document<html>再到<body>后就停止了。下一个阶段是“处于目标”阶段,于是事件在<div>上发生,并在事件处理中被看成是冒泡阶段的一部分。然后,冒泡阶段发生,事件又传播回文档。

多数支持DOM事件流的浏览器都实现了一种特写的行为:即使“DOM2级事件”规范明确要求捕获阶段不会涉及事件目标,但IE9、Safari、Chrome、Firefox和Opera9.5及更高版本都会在捕获阶段触发事件对象上的事件。结果,就是有两个机会在目标对象上面操作事件。

事件处理程序

事件就是用户或浏览器自身执行的某种动作。诸如clickloadmouseover,都是事件的名字,而响应某个事件的函数就叫做事件处理程序(或事件侦听器)。事件处理程序的名字以on开关,因此click事件的事件处理程序就是onclickload事件的事件处理程序就是onload。为事件指定处理程序的方式有好几种。

HTML事件处理程序

html
<input type="button" value="Click Me" onclick="alert('Clicked')" /> <button onclick="handleClick()">Button</button> <script> function handleClick () { alert('Button Clicked'); } </script>

这样指定事件处理程序具有一些独到之处。首先,这样会创建一个封装着元素属性值的函数。这个函数中有一个局部变量event,也就是事件对象

html
<!-- 会输出 "click" --> <input type="button" value="Click Me" onclick="alert(event.type)">

通过event变量,可以直接访问事件对象,你不用自己定义它,也不用从函数的参数列表中读取。

在这个函数内部,this值等于事件的目标元素:

html
<!-- 会输出 "Click Me" --> <input type="button" value="Click Me" onclick="alert(this.value)">

关于这个动态创建的函数,另一个有意思的地方就是它扩展作用域的方式。在这个函数内部,可以像访问局部变量一样访问document及该元素本身的成员。这个函数使用with像下面这样扩展作用域:

js
function () { with (document) { with (this) { // 元素属性值 } } }

如此一来,事件处理程序要访问自己的属性就简单多了

html
<!-- 会输出 "Click Me" --> <input type="button" value="Click Me" onclick="alert(value)">

如果当前元素是一个表单输入元素,则作用域中还会包含访问表单元素(父元素)的入口,这个函数就变成了如下所示:

js
function () { with (document) { with (this.form) { with (this) { // 元素属性值 } } } }

实际上,这样扩展作用域的方式,无非就是想让事件处理程序无需引用表单元素就能访问其他表单字段。

html
<form method="post"> <input type="text" name="username" value=""> <input type="button" value="Echo Username" onclick="alert(username.value)"> </form>

不过,在HTML中指定事件处理程序有两个缺点。首先,存在一个时差问题。因为用户可以会在HTML元素一出现在页面就触发相应的事件,但当时的事件处理程序有可能尚不具备执行条件。为此,很多HTML事件处理程序都会被封装在一个try-catch块中,以便错误不会浮出水面:

js
<input type="button" value="Click Me" onclick="try{showMessage();}catch(ex){}">

另一个缺点是,这样扩展事件处理程序的作用域链在不同浏览器中会导致不同结果。不同JavaScript引擎遵循的标识符解析规则略有差异,很可能会在访问非限定对象成员时出错。

最后一个缺点是HTML与JavaScript代码紧密耦合。这正是许多程序开发人员摒弃HTML事件处理程序,转而使用JavaScript指定事件处理程序的原因所在。

DOM0级事件处理程序

通过JavaScript指定事件处理程序的传统方式,就是将一个函数赋值给一个事件处理程序属性。

js
var btn = document.getElementById('myBtn'); btn.onclick = function () { alert('clicked'); }

使用DOM0级方法指定的事件处理程序被认为是元素的方法。因此,这时候的事件处理程序是在元素的作用域中运行;换句话说,程序中的this引用当前元素:

js
var btn = document.getElementById('myBtn'); btn.onclick = function () { alert(this.id); // 'myBtn' }

实际上,可以在事件处理程序中通过this访问元素的任何属性和方法。以这种方式添加事件处理程序会在事件流的冒泡阶段被处理。

也可以删除通过DOM0级方法指定的事件处理程序:

js
btn.onclick = null;

DOM2级事件处理程序

“DOM2级事件”定义了两个方法,用于处理指定和删除事件处理程序的操作:addEventListener()removeEventListener()。所有DOM节点中都包含这两个方法,并且它们都接受3个参数:要处理的事件名、作为事件处理程序的函数和一个布尔值。最后这个布尔值如果是true,表示在捕获阶段调用事件处理程序;如果为false则表示在冒泡阶段调用。

js
var btn = document.getElementById('myBtn'); btn.addEventListener('click', function () { alert('Hello world!'); }, false)

通过addEventListener()添加的事件处理程序只能使用removeEventLisener()来移除;移除时传入的参数与添加时使用的参数相同。这也意味着通过addEventListener()添加的匿名函数将无法移除。

js
var btn = document.getElementById('myBtn'); btn.addEventListener('click', function () { alert('Hello world!'); }, false); // ... // 下面这段代码没有作用 btn.removeEventListener('click', function () { alert('Hello world!'); }, false);
js
function handlerClick () { alert('Hello world!'); } var btn = document.getElementById('myBtn'); btn.addEventListener('click', handlerClick, false); // ... // 下面这段代码有用 btn.removeEventListener('click', handlerClick, false);

大多数情况下,都是将事件处理程序添加到事件流冒泡阶段,这样可以最大限度地兼容各种浏览器。最好只在需要在事件到达目前之前捕获它的时候将事件处理程序添加到捕获阶段。如果不是特别需要,不建议在事件捕获阶段注册事件处理程序。

IE事件处理程序

IE实现了与DOM中类似的两个方法:attachEvent()datachEvent()。这两个方法接受相同的两个参数:事件处理程序名称与事件处理程序函数。由于IE8及更早版本只支持事件冒泡,attachEvent()添加的事件处理程序都会被添加到冒泡阶段。

js
var btn = document.getElementById('myBtn'); btn.attachEvent('onclick', function () { alert('Clicked'); });

注意,attachEvent()的第一个参数是onclick

在IE中使用attachEvent()与使用DOM0级方法的主要区别在于事件处理程序的作用域。在使用attachEvent()方法的情况下,事件处理程序会在全局作用域中运行,因此this等于window

js
var btn = document.getElementById('myBtn'); btn.attachEvent('onclick', function () { alert(this === window); // true });

使用attachEvent()添加的事件可以通过datachEvent()来移除,条件是必须提供相同的参数。

js
var btn = document.getElementById('myBtn'); btn.attachEvent('onclick', handler); // ... btn.datachEvent('onclick', handler);

跨浏览器的事件处理程序

为了以跨浏览器的方式处理事件,不少开发人员会使用能够隔离浏览器差异的JavaScript库,还有一些开发人员会自己开发最合适的事件处理的方法。

js
var EventUtil = { addHandler = function (element, type, handler) { if (element.addEventListener) { element.addEventListener(type, handler, false); } else if (element.attachEvent) { element.attachEvent('on' + type, handler); } else { element['on' + type] = handler; } }, removeHandler = function (element, type, handler) { if (element.removeEventListener) { element.removeEventListener(type, handler, false); } else if (element.datachEvent) { element.datachEvent('on' + type, handler); } else { element['on' + type] = null; } } }

事件对象

在触发DOM上的某个事件时,会产生一个事件对象event,这个对象中包含着所有与事件有关的信息。包括导致事件的元素、事件的类型以及其他特写事件相关的信息。

DOM中的事件对象

兼容DOM的浏览器会将一个event对象传入到事件处理程序中。无论指定事件处理程序时使用什么方法,都会传入event对象

js
var btn = document.getElementById('myBtn'); btn.onclick = function (event) { alert(event.type); // 'click' } btn.addEventListener('click', function (event) { alert(event.type); // 'click' }, false);

在通过HTMl特性指定事件处理程序时,变量event中保存着event对象

html
<input type="button" value="Click Me" onclick="alert(event.type)" />

event对象包含与创建它的特写事件有关的属性和方法。触发的事件类型不一样,可用的属性和方法也不一样。不过,所有事件都会有下表列出的成员:

|属性/方法|类型|读/写|说明|
|-|-|-|-|-|
|bubbles|Boolean|只读|表明事件是否冒泡|
|cancelable|Boolean|只读|表明是否可以取消事件的默认行为|
|currentTarget|Element|只读|正在处理事件的元素|
|defaultPrevented|Boolean|只读|为true表示已经调用preventDefault()|
|detail|Integer|只读|与事件相关的细节信息|
|eventPhase|Integer|只读|调用事件处理程序的阶段:1表示捕获,2表示处于目标,3表示冒泡|
|preventDefault()|Function|只读|取消事件的默认行为。如果cancelable是true,则可以使用这个方法|
|stopImmediatePropagation()|Function|只读|取消事件的进一步捕获或冒泡,同时阻止任何事件处理程序被调用|
|stopPropagation()|Function|只读|取消事件进一步捕获或冒泡。如果bubbles为true,则可以使用这个方法|
|target|Element|只读|事件的目标|
|trusted|Boolean|只读|为true表示事件是浏览器生成的,反之是由开发人员通过JS创建的|
|type|String|只读|被触发的事件的类型|
|view|AbstractView|只读|与事件关联的抽象视图。等同于发生事件的window对象|

在事件处理程序内部,对象this始终等于currentTarget的值,而target则只包含事件的实际目标。

js
var btn = document.getElementById('myBtn'); btn.onclick = function (event) { alert(event.currentTarget === this); // true alert(event.target === this); // true } // 在Id为`myBtn`的元素上点击时 document.body.onclick = function (event) { alert(event.currentTarget === document.body); // true alert(this === document.body); // true alert(event.target === document.getElementById('myBtn')); // true }

阻止事件的默认行为

要阻止特定事件的默认行为,可以使用preventDefault()方法。

html
<a href="http://www.humandetail.com" id="myLink">Go</a>
js
var link = document.getElementById('myLink'); link.onclick = function (event) { event.preventDefault(); // 阻止a标签跳转 }

阻止事件捕获/冒泡

使用stopPropagation()方法,可以停止事件在DOM层次中传播,即取消进一步的事件捕获或冒泡。

js
var btn = document.getElementById('myBtn'); btn.onclick = function (event) { alert('Clicked'); event.stopPropagation(); } document.body.onclick = function (event) { alert('Body clicked'); // 当点击Id为`myBtn`的元素时,这段代码不会执行。 }

事件对象的eventPhase属性,可以用来确定事件当前正位于事件流的哪个阶段。如果在捕获阶段调用的事件处理程序,那么值等于1;如果事件处理程序处于目标对象上,值为2;如果是在冒泡阶段,值为3。这里需要注意的是,尽管“处于目标”发生在冒泡阶段,但eventPhase仍然一直等于2。

js
var btn = document.getElementById('myBtn'); btn.onclick = function (event) { alert(event.eventPhase); // 2 } document.body.addEventListener('click', function (event) { alert(event.eventPhase); // 1 }, true); document.body.onclick = function (event) { alert(event.eventPhase); // 3 }

IE中的事件对象

与访问DOM中的event对象不同,要访问IE中的event对象有几种不同的方式,取决于指定事件处理程序的方法。在使用DOM0級方法添加事件处理程序时,event对象作为window对象的一个属性存在。

js
var btn = document.getElementById('myBtn'); btn.onclick = function () { var event = window.event; alert(event.type); // 'click' }

如果事件处理程序是使用attachEvent()添加的,那么就会有一个event对象作为参数被传入事件处理程序函数中。

js
var btn = document.getElementById('myBtn'); btn.attachEvent('onclick', function (event) { alert(event.type); })

如果是通过HTMl特性指定的事件处理程序,那么还可以通过一个名叫event的变量来访问event对象

html
<input type="button" value="Click Me" onclick="alert(event.type)" />

IE的event对象同样也包含与创建它的事件相关的属性和方法。所有事件对象都会包含下表所列的属性方法:

属性/方法 类型 读/写 说明
cancelBubble Boolean 读/写 默认值为false,但将其设置为true就可以取消事件冒泡
returnValue Boolean 读/写 默认值为true,但将其设置为false就可以取消事件的默认行为
srcElement Element 只读 事件的目标
type String 只读 事件的类型

跨浏览器的事件对象

虽然DOM和IE中的event对象不同,但基于它们之间的相似性依旧可以拿出跨浏览器的方案来。

js
var EventUtil = { addHandler: function (element, type, handler) { // ... }, getEvent: function (event) { return event ? event : window.event; }, getTarget: function (event) { return event.target || event.srcElement; }, preventDefault: function (event) { if (event.preventDefault) { event.preventDefault(); } else { event.returnValue = false; } }, removeHandler: function (element, type, handler) { // ... }, stopPropagation: function (event) { if (event.stopPropagation) { event.stopPropagation(); } else { event.cancelBubble = true; } } }

事件类型

Web浏览器中可能发生的事件有很多类型。“DOM3级事件”规定了以下几类事件:

  • UI(User Interface,用户界面)事件,当用户与页面上的元素交互时触发;
  • 焦点事件,当元素获得或失去焦点时触发;
  • 鼠标事件;
  • 滚轮事件;
  • 文本事件;
  • 键盘事件;
  • 合成事件,当为IME(Input Method Editor,输入法编辑器)输入字符时触发;
  • 变动(mutation)事件,当底层DOM结构发生变化时触发;
  • 变动名称事件,当元素或属性名变动时触发,此类事件已被废弃。

除了这几个事件之外,HTML5也定义了一组事件,而有些浏览器还会在DOM和BOM中实现其他专有事件。

UI事件

UI事件指的是那些不一定与用户操作有关的事件。这些事件在DOM规则出现之前,都是以这种或那种形式存在的,而在DOM规范中保留是为了向后兼容。现有的UI事件如下:

  • DOMActivate:表示元素已经被用户操作(通过鼠标或键盘)激活。这个事件在DOM3级事件中被废弃。
  • load:当页面完全加载后在window上面触发,当所有框架都加载完毕时在框架集上面触发,当图像加载完毕时在<img>元素上面触发,或者当嵌入的内容加载完毕时在<object>元素上面触发。
  • unload:当页面完全卸载后在window上面触发。
  • abort:当用户停止下载过程时,如果嵌入的内容没有加载完,则在<object>元素上面触发。
  • error:当发生错误时触发。
  • select:当用户选择文本框架中的一个或多个字符时触发。
  • resize:当窗口大小变化时触发。
  • scroll:当用户滚动带滚动条的元素中的内容时,在该元素上面触发。

多数这些事件都与window对象或表单控件有关。

除了DOMActivate之外,其他事件在DOM2级事件中都归为HTML事件。

js
var isSupported = document.implementation.hasFeature('HTMLEvents', '2.0'); // 检测是否支持DOM2级事件规定的HTML事件 var isSupported = document.implementation.hasFeature('UIEvent', '3.0'); // 检测是否支持DOM3级事件定义的事件

load事件

JavaScript中最常用的一个事件就是load。当页面完全加载后(包括所有图像、JavaScript文件、CSS等外部资源),就会触发window上面的load事件。有两种定义onload事件处理程序的方式。

1:

js
EventUtil.addHandler(window, 'load', function (event) { alert('Loaded!'); });

2:

html
<!DOCTYPE html> <html> <head> <title>Example</title> </head> <body onload="alert('Loaded!')"> <!-- ... --> </body> </html>

图像上面也可以触发load事件,无论是在DOM中的图像元素还是HTML中的图像元素。

html
<img src="smile.gif" onload="alert('Image loaded!')">

还有一些元素也以非标准方式支持load事件。在IE9+、Firefox、Opera、Chrome和Safari3+及更高版本中,<script>元素也会触发load事件。IE8及更早版本不支持。

IE和Opera还支持<link>元素上的load事件。

unload事件

load事件对应的是unload事件,这个事件在文档被完全卸载后触发。只要用户从一个页面切换到另一个页面,就会发生unload事件。而利用这个事件最多的情况是清除引用,以避免内存泄漏。

js
EventUtil.addHandler(window, 'unload', function (event) { alert('Unloaded!') })

resize事件

当浏览器窗口宽度或高度发生变化时,就会触发resize事件。这个事件在window上面触发。

js
EventUtil.addHandler(window, 'resize', function (event) { alert('窗口大小被改变了!') })

scroll事件

虽然scroll事件是在window对象上发生的,但它实际表示的则是页面中相应元素的变化。在混杂模式下,可以通过<body>元素scrollLeftscrollTop来监控到这一变化;而在标准模式下,除Safari之外的所有浏览器都会通过<html>元素来反映这一变化。

js
EventUtil.addHandler(window, 'scroll', function (event) { if (document.compatMode === 'CSS1Compat') { alert(document.documentElement.scrollTop); } else { alert(document.body.scrollTop); } })

焦点事件

焦点事件会在页面元素获得或失去焦点时触发。利用这些事件并与document.hasFocus()方法及document.activeElement属性配合,可以知晓用户在页面上的选中。有以下6个焦点事件:

  • blur:当元素失去焦点时触发。这个事件不会冒泡;所有浏览器都支持。
  • DOMFocusIn:在元素获得焦点时触发 。这个事件与HTML事件focus等价,但它冒泡。只有Opera支持。DOM3级事件废弃了,选择了focusin
  • DOMFocusOut:在元素失去焦点时触发 。这个事件是blur的通用版本。只有Opera支持。DOM3级事件废弃了,选择了focusout
  • focus:在元素获得焦点时触发。这个事件不会冒泡;所有浏览器都支持。
  • focusin:在元素获得焦点时触发。这个事件与HTML事件focus等价,但它冒泡,支持这个事件的浏览器有IE5.5+、Safari5.1+、Opera11.5+和Chrome。
  • socusout:在元素失焦点时触发。这个事件是blur的通用版本。支持这个事件的浏览器有IE5.5+、Safari5.1+、Opera11.5+和Chrome。

这一类事件中最主要的两个是focusblur,它们都是JavaScript早期就得到所有浏览器支持的事件。这些事件最大问题是它们不冒泡。因此,IE的focusinfocusout与Opera的DOMFocusInDOMFocusOut才会发生重叠。IE的方式最后被DOM3级事件采纳为标准方式。

当焦点从页面的一个元素移动到另一个元素时,会依次触发下列事件:

plain-text
1. focusout 2. focusin 3. blur 4. DOMFocusOut 5. focus 6. DOMFocusIn

要确定浏览器是否支持这些事件,可以使用如下代码:

js
var isSupported = document.implementation.hasFeature('FocusEvent', '3.0');

鼠标与滚轮事件

鼠标事件是Web开发中最常用的一类事件,毕竟鼠标还是最主要的定位设备。DOM3级事件中定义了9个鼠标事件:

  • click:当鼠标单击时或者按下回车键时触发。
  • dblclick:当双击鼠标时触发。
  • mouseup:在用户释放鼠标时触发。
  • mousedown:在用户按下鼠标时触发。
  • mouseenter:在鼠标光标从元素外部首次移动到元素范围之内时触发。
  • mouseleave:在位于元素范围之内的光标移动到元素范围之外触发。
  • mousemove:当鼠标在元素内部移动时重复地触发。
  • mouseout:在鼠标指定位于一个元素上方,然后用户将其移入另一个元素时触发。移入的另一个元素可以位于前一个元素的外部,也可能是这个元素的子元素。
  • mouseover:在鼠标指针位于一个元素外部,然后用户将其首次移入另一个元素边界之内时触发。

页面上的所有元素都支持鼠标事件。除了mouseentermouseleave,所有鼠标事件都会冒泡,也可以被取消,而取消鼠标事件将会影响浏览器的默认行为。取消鼠标事件的默认行为还会影响其他事件,因为鼠标事件与其他事件是密不可分的关系。

只有在同一个元素上相继触发mousedownmouseup事件,才会触发click事件;如果mousedownmouseup中的一个被取消,就不会触发click事件。类似的,只有触发两次click事件,才会触发一次dblclick事件。这4个事件触发的顺序如下:

plain-text
mousedown mouseup click mousedown mouseup click dblclick

显然,clickdblclick事件都会依赖于其他先行事件的触发。

IE8及之前版本中的实现有一个小BUG,因此在双击事件中,会跳过第二个mousedownclick事件,其顺序如下:

plain-text
mousedown mouseup click mouseup dblclick

IE9修复了这个BUG,之后顺序就正确了。

使用以下代码可以检测浏览器是否支持以上DOM2级事件(除dblclickmouseentermouseleave之外):

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

要检测浏览器是否支持上面的所有事件,可以使用以下代码:

js
var isSupported = document.implementation.hasFeature('MouseEvent', '3.0');

鼠标事件中还有一类滚轮事件。而说是一类事件,其实就是一个mousewheel事件。这个事件跟踪鼠标滚轮,类似于Mac的触控板。

客户区坐标位置

鼠标事件都是在浏览器视口中特定位置发生的。这个位置信息保存在事件对象的clientXclientY属性中。所有浏览器都支持这两个属性,它们的值表示事件发生时,鼠标指针在视口中的水平和垂直坐标。

可以使用类似下列代码取得鼠标事件的客户区坐标信息:

js
var div = document.getElementById('myDiv'); EventUtil.addHandler(div, 'click', function (event) { event = EventUtil.getEvent(event); alert(event.clientX, event.clientY); })

注意,这些值中不包括页面滚动的距离,因此这个位置并不表示鼠标在页面上的位置。

页面坐标位置

页面坐标可以通过事件对象的pageXpageY属性获得。

js
var div = document.getElementById('myDiv'); EventUtil.addHandler(div, 'click', function (event) { event = EventUtil.getEvent(event); alert(event.pageX, event.pageY); })

在页面没有滚动的情况下,pageXpageY的值与clientXclientY的值相等。

IE8及更早版本不支持事件对象上的页面坐标,不过使用客户区坐标和滚动信息可以计算出来。这时候需要用到document.body(混杂模式)或document.documentElement(标准模式)中的scrollLeftscrollTop属性。

js
var div = document.getElementById('myDiv'); EventUtil.addHandler(div, 'click', function (event) { event = EventUtil.getEvent(event); var pageX = event.pageX, pageY = event.pageY; if (pageX === undefined) { pageX = event.clientX + (document.body.scrollLeft || document.documentElement.scrollLeft) } if (pageY === undefined) { pageY = event.clientY + (document.body.scrollTop || document.documentElement.scrollTop) } alert(pageX, pageY); });

屏幕坐标位置

鼠标事件发生时,不仅会有相对于浏览器窗口的位置,还有一个相对于整个电脑屏幕的位置。而通过screenXscreenY属性就可以确定鼠标发生时鼠标指针相对于整个屏幕的坐标信息。

js
var div = document.getElementById('myDiv'); EventUtil.addHandler(div, 'click', function (event) { event = EventUtil.getEvent(event); alert(event.screenX, event.screenY); });

修改键

虽然鼠标事件主要是使用鼠标来触发的,但在按下鼠标时键盘上的某些键的状态也可以影响到所要采取的操作。这些修改键就是Shift、Ctrl、Alt和Meta(在win下是win键,在苹果电脑下是Cmd键),它们经常用来修改鼠标事件的行为。

DOM为此规定了4个属性,表示这些修改键的状态:shiftKeyctrlKeyaltKeymetaKey。这些属性中包含的都是布尔值,如果相应的键按下了,则为true,反之为false。

js
var div = document.getElementById('myDiv'); EventUtil.addHandler(div, 'click', function (event) { event = EventUtil.getEvent(event); var keys = new Array(); if (event.shiftKey) { keys.push('shift'); } if (event.ctrlKey) { keys.push('ctrl'); } if (event.altKey) { keys.push('alt'); } if (event.metaKey) { keys.push('meta'); } alert('Keys: ' + keys.join(',')); });

相关元素

在发生mouseovermouseout事件时,还会涉及更多的元素。这两个事件都会涉及把鼠标指针从一个元素的边界之内移动到另一个元素的边界之内。对于mouseover事件而言,事件的主目标是获得光标的元素,而相关元素就是那个失去光标的元素;mouseout则反过来。

DOM通过event对象的relatedTarget属性提供了相关元素的信息。这个属性只对mouseovermouseout事件才包含值,对于其他事件,这个属性的值是null。Ie8及之前版本不支持,但提供了保存同样信息的不同属性。在mouseover事件触发时,IE的fromElement属性中保存相关元素;在mouseout事件触发时,IE的toElement属性中保存相关元素。

跨浏览器的做法:

js
var EventUtil = { // ... 省略 getRelatedTarget: function (event) { if (event.relatedTarget) { return event.relatedTarget; } else if (event.toElement) { return event.toElement; } else if (event.fromElement) { return event.fromElement; } else { return null; } }, // ... 省略 }

鼠标按钮

只有在主鼠标按钮被单击(或回车键被按下)时才会触发click事件,因此检测按钮的信息并不是必要的。但对于mousedownmouseup事件来说,则在其event对象存在一个button属性,表示按下或释放的按钮。DOM的button属性有如下3个值:0表示主鼠标按钮,1表示中间的鼠标按钮(鼠标滚轮按钮),2表示次鼠标按钮。在常规设置中,主鼠标按钮是鼠标左键,次鼠标按钮则是鼠标右键。

IE8及之前版本也提供了button属性,但这个属性与DOM的button属性有很大差异:

  • 0:表示没有按下按钮。
  • 1:按下主按钮
  • 2:按下次按钮
  • 3:同时按下主、次按钮
  • 4:按下中间按钮
  • 5:同时按下主按钮、中按钮
  • 6:同时按下次按钮中按钮
  • 7:同时按下三个鼠标按钮
js
var EventUtil = { // ... // 将IE模型规范为DOM方式 getButton: function (event) { if (document.implementation.hasFeature('MouseEvenets', '2.0')) { return event.button; } else { switch(event.button) { case 0: case 1: case 3: case 5: case 7: return 0; case 2: case 6: return 2; case 4: return 1; } } }, // ... }

更多的事件信息

“DOM2级事件”规范在event对象中还提供了detail属性,用于给出有关事件的更多信息。对于鼠标事件来说,detail中包含了一个数值,表示在给定位置上发生了多少次单击。在同一个元素上相继地发生一次mousedown和一次mouseup事件算作一次单击。detail属性从1开始计数,每次单击发生后都会递增。如果鼠标在mousedownmouseup之间移动了位置,则detail会重置为0。

IE也通过下列属性为鼠标事件提供了更多信息:

  • altLeft:布尔值,表示是否按下了Alt键。
  • ctrlLeft
  • shiftLeft
  • offsetX:光标相对于目标元素边界的x坐标
  • offsetY

鼠标滚轮事件

IE6首先实现了mousewheel事件。此后,Opera、Chrome和Safari也都实现了这个事件。这个事件可以在任何元素上面触发,最终会冒泡到document(IE8)window(IE9、Opera、Chrome和Safari)。与mousewheel事件对象的event对象除包含鼠标事件的所有标准信息外,还包含一个特殊的wheelDelta属性。当用户向前滚动鼠标滚轮时,wheelDelta是120的倍数;当用户向后滚动时,则是-120的倍数。

有一点需要注意:在Opera9.5之前版本中,wheelDelta值的正负号是颠倒的。

js
EventUtil.addHandler(document, 'mousewheel', function (event) { event = EventUtil.getEvent(event); var delta = (client.engine.opera && client.engine.opera < 9.5 ? -event.wheelDelta : event.wheelDelta); alert(delta); });

Firefox支持一个名为DOMMouseScroll的类似事件,也是在鼠标滚轮滚动时触发。与mousewheel事件一样,DOMMouseScroll也被视为鼠标事件,因而包含与鼠标事件有关的所有。而有关鼠标滚轮的信息则保存在detail属性中,当向前滚动时,值为-3的倍数,当向后滚动时,值为3的倍数。

js
var EventUtil = { // ... getWheelDelta: function (event) { if (event.wheelDelta) { return client.engine.opera && client.engine.opera < 9.5 ? -event.wheelDelta : event.wheelDelta; } else { return -event.detail * 40; } }, // ... }

触摸设置

IOS和Android设备的实现非常特别,因为这些设备没有鼠标。在面向Iphone和Ipod中的Safari开发时,要记住以下几点:

  • 不支持dblclick事件
  • 轻击可单击元素会触发mousemove事件。如果此操作会导致内容发生变化,将不再有其他事件发生;如果屏幕没有因此变化,那么会依次发生mousedownmouseupclick事件。轻击不可单击的元素不会触发任何事件。可单击的元素拇那些单击可产生默认操作的元素。
  • mousemove事件也会触发mouseovermouseout事件。
  • 两个手指放在屏幕上且页面随手指移动而滚动时会触发mousewheelscroll事件。

无障碍性问题

如果你的Web应用程序或网站要确保残疾人特别是那些使用屏幕阅读器的人都能访问,那么在使用鼠标事件时要格外小心。前面提到过,可以通过键盘上的回车键来触发click事件,但其他鼠标事件却无法通过键盘来触发。为此,不建议使用click之外的其他鼠标事件来展示功能或引发代码执行。

键盘与文本事件

用户在使用键盘时会触发键盘事件。“DOM3级事件”为键盘事件制定了规范,IE9率先完全实现了该规范。其他浏览器也在着手实现这一标准,但仍然有很多遗留的问题。

有3个键盘事件:

  • keydown:当用户按下键盘上的任意键时触发,而且如果按住不放的话,会重复触发此事件。
  • keypress:当用户按下键盘上的字符键时触发,按住不放,会重复触发。按下ESC键也会触发这个事件。
  • keyup:当用户释放键盘上的键时触发。

虽然所有元素都支持以上3个事件,但只有在用户通过文本框输入文本时最常用到。

只有一个文本事件:textInput。这个事件是对keypress的补充,用意是在将文本显示给用户之前更容易拦截文本。在文本插入文本框之前会触发textInput事件

键码

在发生keydownkeyup事件时,event对象的keyCode属性中会包含一个代码,与键盘上一个特定的键对应。

键码 键码
退格(Backspace) 8 数字小键盘0 96
制表(Tab) 9 数字小键盘1 97
回车(Enter) 13 数字小键盘2 98
上档(Shift) 16 数字小键盘3 99
控制(Ctrl) 17 数字小键盘4 100
Alt 18 数字小键盘5 101
暂停/中断(Pause/Break) 19 数字小键盘6 102
大写锁定(Caps Lock) 20 数字小键盘7 103
退出(Esc) 27 数字小键盘8 104
上翻页(Page Up) 33 数字小键盘9 105
下翻页(Page Down) 34 数字小键盘+ 107
结尾(End) 35 数字小键盘及大键盘- 109
开头(Home) 36 数字小键盘. 110
左箭头 37 数字小键盘/ 111
上箭头 38 F1 112
右箭头 39 F2 113
下箭头 40 F3 114
插入(Ins) 45 F4 115
删除(Del) 46 F5 116
左Win键 91 F6 117
右Win键 92 F7 118
上下文菜单键 93 F8 119
数字锁(Num Lock) 144 F9 120
滚动锁(Scroll Lock) 145 F10 121
分号(IE/Safari/Chrome) 186 F11 122
分号(Opera/Firefox) 59 F12 123
小于 188 正斜杠 191
大于 190 沉音符(`) 192
等于 61 左方括号 219
反斜杠(\) 220 右方括号 221
单引号 222

字符编码

发生keypress事件意味着按下的键会影响到屏幕中文本的显示。在所有浏览器中,按下能够插入或删除字符的键会触发keypress事件

IE9、Firefox、Chrome和Safari的event对象都支持一个charCode属性,这个属性只有在发生keypress事件时才包含值,而且这个值是按下的那个键所代码字符的ASCII编码。此时的keyCode通常等于0或者也可能等于所按键的键码。

要想以跨浏览器的方式取得字符编码,必须首先检测charCode属性是否可用。

js
var EventUtil = { // ... getCharCode: function (event) { if (typeof event.charCode === 'number') { return event.charCode; } else { return event.keyCode; } } // ... }

DOM3级变化

尽管所有浏览器都实现了某种形式的键盘事件,DOM3级事件还是做出了一些改变。比如,DOM3级事件中的键盘事件,不再包含charChode属性,而是包含两个新属性:keychar

其中,key属性是为了取代keyCode而新增的,它的值是一个字符串。在按下某个字符键时,key的值就是相应的文本字符(如“k”或“M”);在按下非字符键时,key的值是相应键的名(如“Shift”或“Down”)。而char属性在按下字符键时的行为与key相同,但在按下非字符键时值为null。

IE9支持key属性,但不支持char属性。Safari5和Chrome支持名为keyIdentifier的属性,在按下非字符键的情况下与key的值相同。对于字符键,keyIdentifier返回一个格式类似U+0000的字符串,表示Unicode值。

js
var textbox = document.getElementById('myText'); EventUtil.addHandler(textbox, 'keypress', function (event) { var identifier = event.key || event.keyIdentifier; if (identifier) { alert(identifier); } });

由于存在跨浏览器问题,因此不推荐使用keykeyIdentifierchar

DOM3级事件还添加了一个名为location的属性,这是一个数值,表示按下了什么位置上的键:0表示默认键盘,1表示左侧位置(例如左位的Alt键),2表示右侧位置,3表示数字小键盘,4表示移动设备键盘,5表示手柄。IE9支持这个属性。Safari和Chrome支持名为keyLocation的等价属性,但有bug——值始终是0,除非按下数字键盘时(此时值为3);不则,不会是1、2、4、5。同样不推荐使用这个属性。

最后给event对象添加了getModifierState()方法。这个方法接收一个参数,即等于Shift、Control、AltGraph或Meta的字符串,表示要检测的修改键。IE9是唯一支持getModifierState()的浏览器。

textInput事件

当用户在可编辑区域中输入字符时,就会触发textInput事件。这个用于替代keypresstextInput事件的行为稍有不同。区别之一就是任何可以获得焦点的元素都可以触发keypress事件,但只有可编辑区域才能触发textInput事件;区别之二是textInput事件只会在用户按下能够输入实际字符的键才会触发,而keypress事件则在按下那些能够影响文本显示的键时也会触发(例如退格键)。

由于textInput事件主要考虑的是字符,因此它的event对象中还包含一个data属性,这个属性的值就是用户输入的字符(而非字符编码)。

js
var textbox = document.getElementById('myText'); EventUtil.addHandler(textbox, 'textInput', function (event) { event = EventUtil.getEvent(event); alert(event.data); });

另外,event对象上还有一个属性,叫inputMethod,表示把文本输入到文本框中的方式:

  • 0,表示浏览器不确定是怎么输入的。
  • 1,表示是使用键盘输入的。
  • 2,表示文本是粘贴进来的。
  • 3,表示文本是拖放进来的。
  • 4,表示文本是使用IME输入的。
  • 5,表示文本是通过在表单中选择某一项输入的。
  • 6,表示文本是通过手写输入的。
  • 7,表示通过语音输入的。
  • 8,表示通过几种方法组合输入的。
  • 9,表示通过脚本输入的。

使用这个属性可以确定文本是如何输入到控件中的,从而可以验证其有效性。支持textInput的浏览器有IE9+、Safari和Chrome。只有IE支持inputMethod

设备中的键盘事件

(略)

未完待续