重拾JavaScript(十一)-事件

事件

JavaScript和HTML的交互是通过事件实现的。用户在浏览器上的任何操作,都可能触发一个事件,如鼠标移动。滚动页面、点击、甚至鼠标放在页面上的某处等任何的页面操作都会触发一个事件。

事件流

事件流描述的是从页面中接受事件的顺序。IE的事件流是 事件冒泡流,标准的浏览器事件流是 事件捕获流

事件冒泡

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

1
2
3
4
5
6
7
8
9
10
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>event bubbing</title>
</head>
<body>
<div id="div">click me</div>
</body>
</html>

当点击<div>元素时,事件是按照以下顺序传播:

1
2
3
4
1. <div>
2. <body>
3. <html>
4. document

现代浏览器都支持事件冒泡,IE9、Firefox、Chrome和Safari则将事件一直冒泡到window对象。

事件捕获

事件捕获(event capturing)的原理刚好和事件冒泡相反,它的用意在于在事件到达预定目标之前捕获它,而最具体的节点应该是最后才接收到事件的。

当点击<div>元素时,事件是按照以下顺序传播:

1
2
3
4
1. document
2. <html>
3. <body>
4. <div>

IE9、Firefox、Chrome和Safari目前也支持这种事件流模型,但是有些老版本的浏览器不支持,所以很少人使用事件捕获,而是用事件冒泡的多一点。

DOM事件流

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

多数支持DOM事件流的浏览器都实现了一种特定的行为;

即使“DOM2级事件”规范明确要求捕获阶段不会涉及事件目标,但IE9、Safari、Chrome、Firefox和Opera9.5及更高版本都会在捕获阶段触发事件对象上的事件。结果,就是有两个机会在目标对象上操作事件。

事件处理程序

事件就是用户或者浏览器自身执行某种动作,比如click、load、mouseover,都是事件的名字。而响应某个事件的函数就叫做事件处理程序(事件监听器),事件处理程序的名字以on开头,click的事件处理程序是onclick、load的事件处理程序是onload。

HTML事件处理程序

元素支持的每个事件都可以使用一个相应事件处理程序同名的HTML属性指定。这个属性的值应该是可以执行的JavaScript代码,我们可以为一个button添加click事件处理程序

1
<input type="button" value="Click Here" onclick="alert('Clicked!');" />

在HTML事件处理程序中可以包含要执行的具体动作,也可以调用在页面其它地方定义的脚本,刚才的例子可以写成这样

1
<input type="button" value="Click Here" onclick="showMessage();" />

在HTML中指定事件处理程序书写很方便,但是有两个缺点。

  1. 存在加载顺序问题,如果事件处理程序在html代码之后加载,用户可能在事件处理程序还未加载完成时就点击按钮之类的触发事件,存在时间差问题。
  2. 这样书写html代码和JavaScript代码紧密耦合,维护不方便。

DOM0级事件处理程序

通过JavaScript指定事件处理程序就是把一个方法赋值给一个元素的事件处理程序属性。

每个元素都有自己的事件处理程序属性,这些属性名称通常为小写,如onclick等,将这些属性的值设置为一个函数,就可以指定事件处理程序,如下

1
2
3
4
5
6
7
8
<input id="btnClick" type="button" value="Click Here" />

<script type="text/javascript">
var btnClick = document.getElementById('btnClick');
btnClick.onclick = function showMessage() {
alert(this.id);
};
</script>

这样处理,事件处理程序被认为是元素的方法,事件处理程序在元素的作用域下运行,this就是当前元素,所以点击button结果是:btnClick

这样还有一个好处,我们可以删除事件处理程序,只需把元素的onclick属性赋为null即可:

btn.onclick = null; //删除事件处理程序

DOM2级事件处理程序

DOM2级事件定义了两个方法用于处理指定和删除事件处理程序的操作:

  1. addEventListener
  2. removeEventListener

所有的DOM节点都包含这两个方法,并且它们都接受三个参数:

  1. 事件类型
  2. 事件处理方法
  3. 布尔参数,如果是true表示在捕获阶段调用事件处理程序,如果是false,则是在事件冒泡阶段处理
1
2
3
4
5
6
7
8
<input id="btnClick" type="button" value="Click Here" />

<script type="text/javascript">
var btnClick = document.getElementById('btnClick');
btnClick.addEventListener('click', function() {
alert(this.id);
}, false);
</script>

上面代码为button添加了click事件的处理程序,而且该事件会在冒泡阶段被触发,与DOM0级方法一样,这理添加的事件处理程序也是在其依附的元素的作用域中运行,不过有一个好处,我们可以为click事件添加多个处理程序,例如:

1
2
3
4
5
6
7
8
9
10
11
12
13
<input id="btnClick" type="button" value="Click Here" />

<script type="text/javascript">
var btnClick = document.getElementById('btnClick');

btnClick.addEventListener('click', function() {
alert(this.id);
}, false);

btnClick.addEventListener('click', function() {
alert('Hello!');
}, false);
</script>

这样两个事件处理程序会在用户点击button后按照添加顺序依次执行。

通过addEventListener添加的事件处理程序只能通过removeEventListener移除,移除时参数与添加的时候相同,这就意味着刚才我们添加的匿名函数无法移除,因为匿名函数虽然方法体一样,但是句柄却不相同,所以当我们有移除事件处理程序的时候可以这样写:

1
2
3
4
5
6
7
8
9
10
11
12
<input id="btnClick" type="button" value="Click Here" />

<script type="text/javascript">
var btnClick = document.getElementById('btnClick');

var handler=function() {
alert(this.id);
}

btnClick.addEventListener('click', handler, false);
btnClick.removeEventListener('click', handler, false);
</script>

大多数情况下,都是将事件处理程序添加到事件流的冒泡阶段,这样可以最大限度地兼容各种浏览器。


事件传播过程在线实例:事件传播过程

使用stopPropagation阻止事件传播:阻止事件传播


IE事件处理程序

IE实现了与DOM中类似的两个方法:attachEvent()和detachEvent()。这两个方法接受相同的两个参数:1. 事件处理程序名称 2. 事件处理程序方法。

由于IE支持事件冒泡,所以添加的程序会被添加到冒泡阶段,使用attachEvent可以添加事件处理程序:

1
2
3
4
5
6
7
8
9
<input id="btnClick"  type="button" value="Click Here" />

<script>
var btnClick = document.getElementById('btnClick')
var handler = function(){
alert(this.id)
}
btnClick.attachEvent('onclick',handler);
</script>

attachEvent只能在冒泡阶段监听事件,只有两个参数:(事件类型,事件处理函数)

IE中的this:它所得到的并不是当前元素,而是window对象,返回值则为undefined

跨浏览器的事件处理程序

不同的浏览器下处理事件处理程序的区别:

在添加事件处理程序事addEventListener和attachEvent主要有几个区别

  1. 参数个数不相同,这个最直观,addEventListener有三个参数,attachEvent只有两个,attachEvent添加的事件处理程序只能发生在冒泡阶段,addEventListener第三个参数可以决定添加的事件处理程序是在捕获阶段还是冒泡阶段处理(我们一般为了浏览器兼容性都设置为冒泡阶段)
  2. 第一个参数意义不同,addEventListener第一个参数是事件类型(比如click,load),而attachEvent第一个参数指明的是事件处理函数名称(onclick,onload)
  3. 事件处理程序的作用域不相同,addEventListener的作用域是元素本身,this是指的触发元素,而attachEvent事件处理程序会在全局变量内运行,this是window,所以刚才例子才会返回undefined,而不是元素id
  4. 为一个事件添加多个事件处理程序时,执行顺序不同,addEventListener添加会按照添加顺序执行,而attachEvent添加多个事件处理程序时顺序无规律(添加的方法少的时候大多是按添加顺序的反顺序执行的,但是添加的多了就无规律了),所以添加多个的时候,不依赖执行顺序的还好,若是依赖于函数执行顺序,最好自己处理,不要指望浏览器

事件对象

在触发DOM上的某个事件的时候会产生一个事件对象event,这个对象包含着所有与事件有关的信息,包括产生事件的元素、事件类型等相关信息。例如,鼠标所有浏览器都支持event对象,但支持方式不同。

DOM中的事件对象

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

1
2
3
4
5
6
7
8
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 对象。

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

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

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

在事件处理程序内部,this始终等同于currentTarget,而target是事件的实际目标。

要阻止事件的默认行为,可以使用preventDefault()方法,前提是cancelable值为true,比如我们可以阻止链接导航这一默认行为

1
2
3
document.querySelector('#btn').onclick = function (e) {
e.preventDefault();
}

stopPropagation()方法可以停止事件在DOM层次的传播,即取消进一步的事件捕获或冒泡。我们可以在button的事件处理程序中调用stopPropagation()从而避免注册在body上的事件发生

1
2
3
4
5
6
7
8
var handler = function (e) {
alert(e.type);
e.stopPropagation();
}

addEvent(document.body, 'click', function () { alert('Clicked body')});
var btnClick = document.getElementById('btnClick');
addEvent(btnClick, 'click', handler);

若是注释掉e.stopPropagation(); 在点击button的时候,由于事件冒泡,body的click事件也会触发,但是调用这句后,事件会停止传播。

IE中的事件对象

在使用DOM0级方法添加事件处理程序时, event 对象作为 window 对象的一个属性存在。

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

在使用DOM2级方法添加事件处理程序时, event 对象作为参数被传入事件处理程序函数中。

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

在HTML特性指定的事件处理程序中,通过 event 变量来访问 event对象(与 DOM中的事件模型相同)。

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

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

属性/方法 类型 读/写 说明
cancelBubble Boolean 读/写 默认为false,设置为true后可以取消事件冒泡
returnValue Boolean 读/写 默认为true,设为false可以取消事件默认行为
srcElement Element 只读 事件的目标元素
type String 只读 被触发的事件类型

事件类型

常见的事件类型

鼠标事件

  • click:按下鼠标(通常是按下主按钮)时触发。
  • dblclick:在同一个元素上双击鼠标时触发。
  • mousedown:按下鼠标键时触发。
  • mouseup:释放按下的鼠标键时触发。
  • mousemove:当鼠标在一个节点内部移动时触发。当鼠标持续移动时,该事件会连续触发。为了避免性能问题,建议对该事件的监听函数做一些限定,比如限定一段时间内只能运行一次。
  • mouseenter:鼠标进入一个节点时触发,进入子节点不会触发这个事件。
  • mouseover:鼠标进入一个节点时触发,进入子节点会再一次触发这个事件。
  • mouseout:鼠标离开一个节点时触发,离开父节点也会触发这个事件。
  • mouseleave:鼠标离开一个节点时触发,离开父节点不会触发这个事件。
  • contextmenu:按下鼠标右键时(上下文菜单出现前)触发,或者按下“上下文菜单键”时触发。
  • wheel:滚动鼠标的滚轮时触发,该事件继承的是WheelEvent接口。

键盘事件

  • keydown:按下键盘时触发。
  • keypress:按下有值的键时触发,即按下 Ctrl、Alt、Shift、Meta 这样无值的键,这个事件不会触发。对于有值的键,按下时先触发keydown事件,再触发这个事件。
  • keyup:松开键盘时触发该事件。

触摸事件

  • touchstart:用户开始触摸时触发,它的target属性返回发生触摸的元素节点。
  • touchend:用户不再接触触摸屏时(或者移出屏幕边缘时)触发。
  • touchmove:用户移动触摸点时触发,如果触摸的半径、角度、力度发生变化,也会触发该事件。

资源事件

  • beforeunload:在窗口、文档、各种资源将要卸载前触发。它可以用来防止用户不小心卸载资源。
  • unload:在窗口关闭或者document对象将要卸载时触发。
  • load:在页面或某个资源加载成功时触发。注意,页面或资源从浏览器缓存加载,并不会触发load事件。

窗口事件

  • scroll:在文档或文档元素滚动时触发,主要出现在用户拖动滚动条。
  • resize:在改变浏览器窗口大小时触发,主要发生在window对象上面。

拖动事件

  • drag:当某个对象被拖动时触发此事件。
  • dragstart:当某对象将被拖动时触发此事件。
  • dragend:当鼠标拖动结束时触发此事件。
  • dragenter:当对象被鼠标拖动的对象进入其容器范围内时触发此事件。
  • dragover:当某被拖动的对象在另一对象容器范围内拖动时触发此事件。
  • dragleave:当对象被鼠标拖动的对象离开其容器范围内时触发此事件。
  • drop:在一个拖动过程中,释放鼠标键时触发此事件。

自定义事件

监听click事件,当用户监听的时候,就会有响应

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
var EventCenter = {
on: function(type, handler){
document.addEventListener(type, handler)
},
fire: function(type, data){
return document.dispatchEvent(new CustomEvent(type, {
detail: data
}))
}
}

//绑定的事件
EventCenter.on('hello', function(e){
console.log(e.detail)
})
//用户所执行的交互行为
EventCenter.fire('hello', '你好')

实现对象分析:

对象EventCenter中有两个函数,对应的有两个属性:on、fire

事件方法分析:

EventCenter.on,一个对象里有一个on方法,当EventCenter听到’hello’事件时,去执行function(e)这个事件监听函数,通过e.detail获取该事件的内容。此为绑定的事件概述

绑定好之后,在某些场景下,用户执行EventCenter.fire(‘hello’, ‘你好’),则会执行事件’hello’,后面加一些内容,如’你好’。与之对应的则是刚才的e.detail内容。


更多完整的事件类型:事件类型一览

一些主要事件的属性和方法:事件种类