事件
事件是JavaScript跟HTML交互实现的一种机制。在文档或浏览器进行用户交互或其它的某些特定瞬间,会触发事件,然后JavaScript响应这些事件。在设计模式上看,这类似于观察者模式。JavaScript 和 HTML 之间是 松耦合的。
到今天为止,虽然不同的浏览器(IE9+、Chrome、Firefox、Opera、Safira)基本上都实现了 “DOM2级事件” 的 核心部分。但这个规范本身并没有涵盖所有事件类型,且浏览器对象模型(BOM)也支持一些事件————这些事件与文档对象模型 (DOM)事件之间的关系并不十分清晰。 再到后来 DOM3 级的出现,增强后的 DOM 事件 API 变得更加繁琐。使用事件有时相对简单, 有时则非常复杂,难易程度会因你的需求而不同。不过,有关事件的一些核心概念是一定要理解的。
事件流
事件流描述的是从页面中接收事件的顺序。举个例子说明一下事件流的概念,假如页面上有一堆div,一层一层嵌套。如果你点击了最深一层的div,那么最外层的div也会相应你的点击事件。在这一点上,所有的浏览器厂商都没有异议。不同的是,事件在每一层div的响应顺序。有的观念认为,事件是从最深一层的div开始响应,一层层往外走,直到最顶层的div(这个过程就是后面说的事件冒泡)。也有认为观念认为,事件触发顺序是完全相反的顺序。即事件首先从最外层的div响应,一层层往里面的div传递。直到用户点击的那一层的div(这个就是后面说到的事件捕获)。
综上所述,事件流大致分为两种类型:
- 事件冒泡:所有现代浏览器都支持事件冒泡,IE9+、Firefox、Chrome 和 Safari 会将事件一直 冒泡到 window 对象。
- 事件捕获:“DOM2 级事件”规范要求事件应该从 document 对象 开始传播,但现代浏览器都是从 window 对象开始捕获事件的。
由于老版浏览器不支持事件捕获,所有在实际场景中,一般建议使用事件冒泡,有特殊情况再使用事件捕获。
DOM2定义的事件流
DOM2规范定义事件流的三个阶段: 事件捕获阶段、处于事件目标对象阶段、事件冒泡阶段。
首先发生的是事件捕获,为截获事件提供了机会。然后是实际的目标接收到事件。最后一个阶段是冒泡阶段,可以在这个阶段对事件做出响应。
“DOM2 级事件”规范明确要求捕获阶段不会涉及事件目标,但 IE9、Safari、Chrome、Firefox 和 Opera 9.5 及更高版本都会在捕获阶段触发事件对象上的事件。结果,就是有两个机会在目标对象上面操作事件。
事件处理程序
HTML事件处理程序
某个元素支持的每种事件,都可以使用一个与相应事件处理程序同名的HTML特性来指定。这个 特性的值应该是能够执行的JavaScript代码。这种方式比较古老,存在许多问题:
- 加载顺序不同产生的时差问题:用户可能会在 HTML 元素一出现在页面上就触发相应的事件,但当时的事件处理程序有可能尚不具备执行条件;
- 不同的浏览器对事件处理程序扩展的作用域链是不同的。详细介绍可以参考元素的内联事件处理函数的特殊作用域链在各浏览器中存在差异 和 Event Handler Scope;
- HTML 与 JavaScript 代码紧密耦合,导致程序的维护困难;
<input type="button" value="click me" onclick="alert(12345);">
<script>
// 事件处理函数定义在JavaScript中,和DOM对象分离,但是处理逻辑又相关,导致耦合性比较强;
// 另一个问题就是 实际加载顺序可能导致在用户点击 DOM对象时, 事件处理方法对应的代码还未被解析,导致程序直接报错;
function myalert(event){
console.log(event.type);
}
</script>
<input type="button" value="click me" onclick="myalert(event)">
DOM0级事件处理方法
在JavaScript代码中,取得DOM对象的引用,然后将一个函数赋值给事件处理程序属性。这种方式简单且跨浏览器。
使用 DOM0 级方法指定的事件处理程序被认为是元素的方法。因此,这时候的事件处理程序是在元素的作用域中运行;换句话说,程序中的 this 引用当前元素。
var input = document.getElementsByTagName('input')[0];
input.onclick = function (event) {
console.log(event.type);
console.log(this);
}
删除对应的事件处理方法,只需要直接赋值null即可。
input.onclick = null
DOM2级事件处理方法
“DOM2 级事件”定义了两个方法,用于处理指定和删除事件处理程序的操作:addEventListener()
和 removeEventListener()
。
这两个方法都接受三个参数: 事件名、事件处理程序的函数、一个布尔值(表示事件处理是否在捕获阶段进行)。
var dom2 = document.getElementById('dom2');
dom2.addEventListener('click', function(event){
console.log(event.type);
console.log(this);
});
// DOM2 级方法添加事件处理程序的主要好处是可以添加多个事件处理程序。
function second(){
console.log("this is second event handler");
}
dom2.addEventListener('click', second)
使用 DOM2 级方法添加事件处理程序的主要好处是可以添加多个事件处理程序。
删除事件
通过 addEventListener()添加的事件处理程序只能使用 removeEventListener()来移除;移 除时传入的参数与添加处理程序时使用的参数相同。这也意味着通过 addEventListener()添加的匿 名函数将无法移除。
dom2.removeEventListener('click', second);
IE9、Firefox、Safari、Chrome 和 Opera 支持 DOM2 级事件处理程序。
IE事件处理程序
对于IE8及以下的浏览器可以使用IE浏览器提供的attachEvent()
和 detachEvent()
。这两个方法对应DOM2 中的两个方法。在使用细节上有些许差异。
在使用 attachEvent()方 法的情况下,事件处理程序会在全局作用域中运行,因此 this 等于 window。
支持 IE 事件处理程序的浏览器有 IE 和 Opera。
跨浏览器的事件处理程序
function addEventListener(dom, type, fn, bool){
if(dom.addEventListener){
dom.addEventListener(type, fn, bool)
} else if( dom.attachEvent){
dom.attachEvent('on' + type, fn, bool);
} else {
dom['on' + type] = fn;
}
}
function removeEventListener(dom, type, fn, bool){
if(dom.removeEventListener){
dom.removeEventListener(type, fn, bool)
} else if( dom.attachEvent){
dom.detachEvent('on' + type, fn, bool);
} else {
dom['on' + type] = null;
}
}
var dom2 = document.getElementById('dom2');
var eventFn = function(){
console.log('click event is fired!!!!');
};
addEventListener(document.getElementById('dom2'), 'click', eventFn);
removeEventListener(document.getElementById('dom2'), 'click', eventFn);
事件对象
在事件被触发时,会生成一个事件对象。这个对象在这之前的例子中,都直接传入到事件处理程序中。这个对象包含着所有与事件有关的信息,包括导致事件的元素、事件的类型以及其他与特定事件相关的信息。下面我们来仔细看看这个事件对象都具体构成。
DOM中的事件对象
兼容 DOM 的浏览器会将一个 event 对象传入到事件处理程序中。无论指定事件处理程序时使用什 么方法(DOM0 级或 DOM2 级),都会传入 event 对象。
event对象都属性:
属性名称 | 含义说明 |
---|---|
bubbles | 只读, 一个布尔值,用来表示该事件是否在DOM中冒泡。 |
cancelBubble | Event.stopPropagation() 以前的别名。通过在一个事件处理程序返回前设置这个属性的值为真,来阻止事件冒泡。 |
cancelable | 只读 一个布尔值,用来表示这个事件是否可以取消。 |
composed | 只读 一个布尔值,用来表示这个事件是否可以在阴影DOM和常规DOM之间的边界上浮动。 |
currentTarget | 只读, 当前注册事件的对象的引用。这是一个这个事件目前需要传递到的对象(译者:大概意思就是注册这个事件监听的对象)。这个值会在传递的途中进行改变。 |
deepPath | 一个由事件流经过了的 DOM Node 组成的 Array |
defaultPrevented | 只读 一个布尔值,表示了是否已经执行过了event.preventDefault()(译者:其实应该就是是否已经阻止默认行为) |
eventPhase | 只读 指示事件流正在处理哪个阶段。 |
explicitOriginalTarget | 只读 事件的原始目标(Mozilla内核特定属性)。 |
originalTarget | 只读 在任何重定向之前,事件的原始目标 (Mozilla内核特定属性)。 |
returnValue | 一个非标准的替代方案(从旧版本的Microsoft Internet Explorer)到Event.preventDefault()和Event.defaultPrevented。 |
scoped 只读 | 一个Boolean,表示给定的事件是否会通过阴影进入到标准的DOM中。 此属性已重命名为composed。 |
srcElement | 非标准别名(Microsoft Internet Explorer的旧版本) Event.target. |
target | 只读 对事件起源目标的引用。 |
timeStamp | 只读 事件创建时的时间戳,毫秒级别。按照规定,这个时间戳是距离某个特定时刻的差值,但实际上在浏览器中此处的事件戳的定义有所不同。另外,正在开展工作将其改为DOMHighResTimeStamp。(译者注:参考时间戳,在浏览器中此处的时间戳是距离该页面打开时刻的大小)。 |
type | 只读 事件的类型(不区分大小写)。 |
isTrusted | 只读 指明事件是否是由浏览器(当用户点击实例后)或者由脚本(使用事件的创建方法,例如event.initEvent)启动。 |
event对象支持的方法:
- event.initEvent : 通过DocumentEvent的接口给被创建的事件初始化某些值。
- event.preventDefault : 取消默认事件(如果该事件可取消)。
- event.stopImmediatePropagation : 对这个特定的事件而言,没有其他监听器被调用。这个事件既不会添加到相同的元素上,也不会添加到以后将要遍历的元素上(例如在捕获阶段)。
- event.stopPropagation : 停止事件冒泡。
IE 中的事件对象
与访问 DOM 中的 event 对象不同,要访问 IE 中的 event 对象有几种不同的方式,取决于指定事 件处理程序的方法。在使用 DOM0 级方法添加事件处理程序时,event 对象作为 window 对象的一个 属性存在。
var btn = document.getElementById("myBtn");
btn.onclick = function(){
var event = window.event;
alert(event.type); //"click"
};
如果事件处理程序是使用 attachEvent()添加的,那 么就会有一个 event 对象作为参数被传入事件处理程序函数中,如下所示:
var btn = document.getElementById("myBtn");
btn.attachEvent("onclick", function(event){
alert(event.type); //"click"
});
属性名 | 类型 | 是否可写 | 含义 |
---|---|---|---|
cancelBubble | Boolean | 读/写 | 默认值为false,但将其设置为true就可以取消事件冒泡(与DOM中 的stopPropagation()方法的作用相同) |
returnValue | Boolean | 读/写 | 默认值为true,但将其设置为false就可以取消事件的默认行为(与 DOM中的preventDefault()方法的作用相同) |
srcElement | Element | 只读 | 事件的目标(与DOM中的target属性相同) |
type | String | 只读 | 被触发的事件类型 |
事件类型
Web 浏览器中可能发生的事件有很多类型。如前所述,不同的事件类型具有不同的信息,而“DOM3 级事件”规定了以下几类事件:
- UI(User Interface,用户界面)事件,当用户与页面上的元素交互时触发;
- 焦点事件,当元素获得或失去焦点时触发;
- 鼠标事件,当用户通过鼠标在页面上执行操作时触发;
- 滚轮事件,当使用鼠标滚轮(或类似设备)时触发;
- 文本事件,当在文档中输入文本时触发;
- 键盘事件,当用户通过键盘在页面上执行操作时触发;
- 合成事件,当为 IME(Input Method Editor,输入法编辑器)输入字符时触发;
- 变动(mutation)事件,当底层 DOM 结构发生变化时触发。
- 变动名称事件,当元素或属性名变动时触发。此类事件已经被废弃,没有任何浏览器实现它们, 因此本章不做介绍。
除了这几类事件之外,HTML5 也定义了一组事件,而有些浏览器还会在 DOM 和 BOM 中实现其他 专有事件。这些专有的事件一般都是根据开发人员需求定制的,没有什么规范,因此不同浏览器的实现 有可能不一致。 DOM3 级事件模块在 DOM2 级事件模块基础上重新定义了这些事件,也添加了一些新事件。包括 IE9 在内的所有主流浏览器都支持 DOM2 级事件。IE9 也支持 DOM3 级事件。
UI事件
UI事件不一定是由用户触发。
- load事件 : JavaScript 中最常用的一个事件就是 load。当页面完全加载后(包括所有图像、JavaScript 文件、 CSS 文件等外部资源),就会触发 window 上面的 load 事件。
- unload事件 : 与 load 事件对应的是 unload 事件,这个事件在文档被完全卸载后触发。只要用户从一个页面切 换到另一个页面,就会发生 unload 事件。而利用这个事件最多的情况是清除引用,以避免内存泄漏。
- scroll事件 : scroll 事件是在 window 对象上发生的,但它实际表示的则是页面中相应元素的变化。
- resize事件 : 当浏览器窗口被调整到一个新的高度或宽度时,就会触发 resize 事件。这个事件在 window(窗口)上面触发。
焦点事件
焦点事件会在页面元素获得或失去焦点时触发。利用这些事件并与 document.hasFocus()方法及 document.activeElement 属性配合,可以知晓用户在页面上的行踪。
- blur:在元素失去焦点时触发。这个事件不会冒泡;所有浏览器都支持它。
- focus:在元素获得焦点时触发。这个事件不会冒泡;所有浏览器都支持它。
- focusin:在元素获得焦点时触发。这个事件与 HTML 事件 focus 等价,但它冒泡。支持这个 事件的浏览器有 IE5.5+、Safari 5.1+、Opera 11.5+和 Chrome。
- focusout:在元素失去焦点时触发。这个事件是 HTML 事件 blur 的通用版本。支持这个事件 的浏览器有 IE5.5+、Safari 5.1+、Opera 11.5+和 Chrome。
最常用的是blur
和 focus
两个事件。
PS:焦点类事件是不会冒泡的。
鼠标与滚轮事件
鼠标事件是Web开发中常用到的事件,因为鼠标主要是还是用作定位设备。
- click:在用户单击主鼠标按钮(一般是左边的按钮)或者按下回车键时触发。这一点对确保 易访问性很重要,意味着 onclick 事件处理程序既可以通过键盘也可以通过鼠标执行。
- dblclick:在用户双击主鼠标按钮(一般是左边的按钮)时触发。从技术上说,这个事件并不 是 DOM2 级事件规范中规定的,但鉴于它得到了广泛支持,所以 DOM3 级事件将其纳入了标准。
- mousedown:在用户按下了任意鼠标按钮时触发。不能通过键盘触发这个事件。
- mouseenter:在鼠标光标从元素外部首次移动到元素范围之内时触发。这个事件不冒泡,而且 在光标移动到后代元素上不会触发。DOM2 级事件并没有定义这个事件,但 DOM3 级事件将它 纳入了规范。IE、Firefox 9+和 Opera 支持这个事件。
- mouseleave:在位于元素上方的鼠标光标移动到元素范围之外时触发。这个事件不冒泡,而且 在光标移动到后代元素上不会触发。DOM2 级事件并没有定义这个事件,但 DOM3 级事件将它 纳入了规范。IE、Firefox 9+和 Opera 支持这个事件。
- mousemove:当鼠标指针在元素内部移动时重复地触发。不能通过键盘触发这个事件。
- mouseout:在鼠标指针位于一个元素上方,然后用户将其移入另一个元素时触发。又移入的另 一个元素可能位于前一个元素的外部,也可能是这个元素的子元素。不能通过键盘触发这个事件。
- mouseover:在鼠标指针位于一个元素外部,然后用户将其首次移入另一个元素边界之内时触 发。不能通过键盘触发这个事件。
- mouseup:在用户释放鼠标按钮时触发。不能通过键盘触发这个事件。
鼠标事件上的常用属性:
- 客户区坐标位置:鼠标事件都是在浏览器视口中的特定位置上发生的。这个位置信息保存在事件对象的 clientX 和 clientY 属性中。
- 页面坐标位置:页面坐标通过事件对象的 pageX 和 pageY 属性,能告诉你事件是在页面中的什么位置发生的。
- 屏幕坐标位置:鼠标事件发生时,有一个相对于整个电脑屏幕的位置。而通 过 screenX 和 screenY 属性就可以确定鼠标事件发生时鼠标指针相对于整个屏幕的坐标信息。
内存和性能
由于事件处理程序可以为现代 Web 应用程序提供交互能力,因此许多开发人员会不分青红皂白地 向页面中添加大量的处理程序。在 JavaScript 中,添加到页面上 的事件处理程序数量将直接关系到页面的整体运行性能。导致这一问题的原因是多方面的。首先,每个 函数都是对象,都会占用内存;内存中的对象越多,性能就越差。其次,必须事先指定所有事件处理程 序而导致的 DOM 访问次数,会延迟整个页面的交互就绪时间。事实上,从如何利用好事件处理程序的 角度出发,还是有一些方法能够提升性能的。
- 事件委托:对“事件处理程序过多”问题的解决方案就是事件委托。事件委托利用了事件冒泡,只指定一个事 件处理程序,就可以管理某一类型的所有事件。
- 移除事件:在不需要的时候移除事件处理程序;内存中留有那些过时不用的“空事件处理程序”(dangling event handler),也是造成 Web 应用程序内存与性能问题的主要原因。
模拟事件
事件,就是网页中某个特别值得关注的瞬间。事件经常由用户操作或通过其他浏览器功能来触发。但很少有人知道,也可以使用 JavaScript 在任意时刻来触发特定的事件,而此时的事件就如同浏览器创建的事件一样。也就是说,这些事件该冒泡还会冒泡,而且照样能够导致浏览器执行已经指定的处理它们的事件处理程序。在测试 Web 应用程序,模拟触发事件是一种极其有用的技术。DOM2 级规为此规定了模拟特定事件的方式,IE9、Opera、Firefox、Chrome 和 Safari 都支持这种方式。IE 有它自己模拟事件的方式。
创建模拟事件可以使用createEvent方法,触发模拟事件使用dispatchEvent方法。
通常的模拟事件一般包括:
- UIEvents:一般化的 UI 事件。鼠标事件和键盘事件都继承自 UI 事件。DOM3 级中是 UIEvent。
- MouseEvents:一般化的鼠标事件。DOM3 级中是 MouseEvent。
- MutationEvents:一般化的 DOM 变动事件。DOM3 级中是 MutationEvent。
- HTMLEvents:一般化的 HTML 事件。没有对应的 DOM3 级事件(HTML 事件被分散到其他类别中)。
var btn = document.getElementById('btn');
// 模拟单击事件
var event = document.createEvent('MouseEvents');
event.initEvent('click', true, true, document.defaultView, 0,0,0,0,0,false, false, false, false, 0, null);
btn.dispatchEvent(event);
initEvent方法的参数是根据创建的事件类型定的。举个例子,如果创建鼠标事件,那么后面的参数,跟真实鼠标事件的event 对象的属性相关。
IE有自己的事件模拟方法,总体使用方式类似DOM2/DOM3的事件模拟规范。
小结
在使用事件时,需要考虑如下一些内存与性能方面的问题。
- 有必要限制一个页面中事件处理程序的数量,数量太多会导致占用大量内存感觉页面反应不够灵敏。
- 建立在事件冒泡机制之上的事件委托技术,可以有效地减少事件处理程序的数量
- 建议在浏览器卸载页面之前移除页面中的所有事件处理程序。