事件

142 阅读10分钟

概述

本文中的 DOM ( 大写 ) 一般指 W3C 中的 DOM 标准。

1. 事件冒泡和事件捕获

早期的两家浏览器厂商( IE 和 Netscape Communicator ) 分别使用了两种事件处理机制( 事件冒泡和事件捕获 )。

1.1 事件冒泡

事件开始时由最具体的元素( 文档中嵌套层次最深的节点,也是 DOM 树中的叶子节点 )接收,然后逐级向上传播到较为不具体的节点。如下面的 DOM 结构

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

如果单击了 <div id="myDiv"> 这个元素,那么这个事件会按照如下顺序传播:

<div> -> <body> -> <html> -> document -> window

IE8 只会冒泡到 document 。

1.2 事件捕获

和事件冒泡恰恰相反,不太具体的节点会更早地接收到事件,最具体的节点会最后接收到事件。在上面的例子中,事件的传播顺序就会变为:

window -> document -> <html> -> <body> -> <div>

值得注意的是,目前的主流浏览器和 IE9+ 都同时支持这两种事件流模型。

1.3 DOM 事件流

“DOM2级事件”标准中规定的事件流应该包括三个阶段:事件捕获阶段、处于目标阶段、事件冒泡阶段。

首先发生的事件捕获,然后是实际的目标接收到事件,最后是冒泡阶段。

在 DOM 事件流,实际的目标( <div> )在捕获阶段不会接收到事件,捕获阶段到 <body> 后就停止。下一阶段 - “处于目标”阶段,事件在 <div> 上发生,并在事件处理中被认为是“冒泡阶段”的一部分。

多数浏览器都可以在捕获阶段和冒泡阶段处理事件。

2. 事件处理程序

2.1 HTML 事件处理程序

HTML 元素支持的每一种事件,都可以使用一个与相应事件处理程序同名的 HTML 特性来指定。如下:

<input type="button" value="Click Me" onclick="alert('Clicked')" />

和其他特性不同的是,由于特性值是 javascript,因此不能再其中使用未经转义的 HTML 语法字符,如 & "" < > 等。

<input type="button" value="Click Me" onclick="alert(&quot;Clicked&quot;)" />

通过这种方式指定事件处理程序有一些独到之处。

  • 这样会创建一个封装着元素属值的函数。即 onclick="alert('Clicked')" 会创建一个下面的函数 function (event) { alert('Clicked'); } 在 HTML 行内使用时,不需要定义 Event 就可以使用它。
  • 这个函数的扩展作用域很神奇。this 会指向目标元素本身,且可以像访问局部变量一样,访问 document 及该元素本身的成员,也就是说下面两端代码是等同的。
<input type="button" value="Click Me" onclick="alert(this.value)" />
<input type="button" value="Click Me" onclick="alert(value)" />

在 HTML 中指定事件处理程序,有如下缺点

  • HTML 的解析需要过程,如果事件触发在解析到指定函数之前的话,就会出问题。
  • 扩展事件处理程序的作用域链在不同的浏览器中会导致不同的结果
  • HTML 和 Javascript 代码过于耦合

2.2 DOM0 级事件处理程序

这是指早期通过 Javascript 指定事件处理程序的方式,将一个函数复制给一个事件处理程序属性。示例如下:

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

每个元素( 包括 window 和 document )都有自己的事件处理程序属性( 这些属性通常小写 )。

和在 HTML 中定义事件处理程序一样,该函数中的 this 指向元素本身,但不能省略。

关于 DOM0 级事件处理程序需要注意以下几点

  • 通过非箭头函数方式定义的方法( 如上述示例代码中方法 ),this 会指向元素本身
  • 这种方式定义的事件处理程序,会在事件流的冒泡阶段处理
  • 重置或修改 DOM0 级方法指定的事件处理程序,只能通过重置事件处理属性的值来进行,如 btn.onclick = null;

2.3 DOM2 级事件处理程序

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

所有的 DOM 节点都包含这两个方法,且这两个方法都接收 3 个参数,先后是: 要处理的事件名、事件处理函数、事件处理阶段( 布尔值,true 代表在捕获阶段调用事件处理函数,false 代表在冒泡阶段调用事件处理函数 )。实际上,最近两年,最后一个参数已经发生了变化,具体 addEventListener 的现阶段完整 api 介绍,可以参考 MDN 手册:developer.mozilla.org/zh-CN/docs/… 。

通过 addEventListener() 添加的事件处理程序只能通过 removeEventListener() 来移除。这句话的更深层次含义是,通过 addEventListener() 添加的匿名函数将无法被移除。建议使用如下代码来操作:

var btn = document.getElementById('myBtn');
var handler = function () {
    alert(this.id);
}
btn.addEventListener('click', handler, false);
btn.removeEventListener('click', handler, false);

在大多数情况下,都是将事件处理程序添加到事件流的冒泡阶段,这样可以最大程度地兼容各类浏览器( 这也是为什么第三个参数默认值是 false 的原因 )。应该按照如下原则处理:除非必须在事件到达目标之前截获它,否则不应该在事件捕获阶段注册事件处理程序

2.4 番外: IE9 以下版本的 IE 事件处理程序

IE8 及以下版本不支持 DOM2 级事件处理程序,单独提供了两个类似的方法:attachEvent()detachEvent()。这两个函数只接受两个参数:事件处理程序名称和事件处理程序函数。因为 IE8 及更早版本只支持事件冒泡,所以通过 attachEvent() 添加的事件处理程序都会被添加到冒泡阶段。

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

注意,和 DOM 事件有以下几点不同

  • 此时的事件处理名称和 DOM2级不同,需要加上 on 前缀。
  • this 不再指向元素本身,而是指向全局作用域
  • 为同一个事件注册的多个事件处理函数,执行顺序和注册顺序相反。

3. 事件对象

就是我们常见的 event 

3.1 DOM 中的事件对象

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

event 对象中包含于此次事件有关的属性和方法。触发的事件类型不同,可用的属性和方法也不一样。具体可以参考 MDN 手册: developer.mozilla.org/zh-CN/docs/… 。

3.1.1 事件处理函数中的 this 和 event.target   event.currentTarget

执行以下代码,然后点击 <input>

<body>
    <input type="button" id="myBtn">
<body>

<script>
    document.body.onclick = function (e) {
        console.log(e.currentTarget === document.body); // true
        console.log(this === document.body); // true
        console.log(e.target === document.getElementById('myBtn')); // true
    }
</script>

  • this 指向当前事件处理程序对应的元素本身
  • event.currentTarget 指向事件处理程序正在处理事件的那个元素
  • event.target 指向事件真是的目标

3.1.2  event.cancelable 和 event.preventDefault()

只有 cancelable 属性为 true 的事件,才能通过 e.preventDefault() 方法取消其默认行为。

3.1.3  event.stopPropagation() 用于立即停止事件在 DOM 层次中的传播,取消进一步的事件捕获或冒泡( 但是要注意当前事件注册的事件阶段 )。

3.2  番外: IE 中的事件对象

3.2.1  访问 event 对象

和 DOM 中的事件对象不同,要访问 IE 中的 event 对象,有几种不同的方式,这取决于注册事件处理程序的方法。

使用 DOM0 级方法添加事件处理程序时

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

使用 attachEvent() 注册的事件处理程序

var btn = document.getElementById('myBtn');
btn.attachEvent('onclick', function (e) {
    alert(e.type); // "click"
}

3.2.2  IE 中事件处理程序的作用域是根据指定它的方式来的,不能像 DOM 事件一样,认为是事件目标

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

btn.attachEvent('onclick', function(e) {
    alert(e.srcElement === this); // false
});

3.3 番外:  跨浏览器的事件对象兼容方案

请参考《JavaScript 高级程序设计》 P360

4. 事件类型

详细请参考《JavaScript 高级程序设计》 P362

4.1 几个比较特殊的事件

  • blur: 在元素失去焦点时触发。但是这个事件不会冒泡
  • focus: 获得焦点;不会冒泡
  • focusin: 获得焦点,会冒泡
  • focusout: 失去焦点,会冒泡

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

focusout ( 上一个元素 ) -> focusin ( 下一个元素 ) -> blur ( 上一个元素 ) -> focus ( 下一个元素 )
  • click: 单击鼠标主键( 一般是左键 )或者回车键触发。这时为了保证易访问性
  • dbclick: 不是 DOM2 级事件规范,但是被广泛支持,在 DOM3 中被纳入标准
  • mousedown: 单击任意鼠标按钮触发。不能通过键盘触发这个事件
  • mouseenter: 鼠标从元素外部首次移动到元素内部时触发。不冒泡,且光标移动到后代元素上不会触发
  • mouseleave: 鼠标从元素内部移动到元素外部时触发。不冒泡,且光标移动到后代元素上不会触发
  • mousemove: 鼠标在元素中移动式,反复触发
  • mouseout: 鼠标从一个元素上方,移动到另一个元素上方时触发。这个“另一个元素”可以位于这个元素的外部,也可以是这个元素的子元素。
  • mouseover: 鼠标位于元素外部,然后用户首次将鼠标移入到另一个元素内时触发。
  • mouseup: 释放鼠标时触发。

页面上所有元素都支持鼠标事件。除了 mouseenter 和 mouseleave,所有的鼠标事件都会冒泡,也可以被取消。取消鼠标事件将会影响浏览器的默认行为,甚至影响其他事件。(比如取消 mousedown 或者 mouseup 就不会触发 click 事件)

  • 键盘和文本事件顺序: keydown( 按下任意键 ) -> keypress( 按下字符键 ) -> keyup
  • 键盘和鼠标事件支持修改键。即对于 shift,ctrl,alt,meta(windows: window, mac: cmd),事件对象中存在对应的 shiftKey,ctrlKey,altKey,metaKey 四个布尔值属性,如果在事件触发时,这些键被按下,对应的属性就为 true。
  • DOM3 级事件中新增了 textInput 事件,在可编辑区域输入字符时,就会触发。基本可以替代 keyPress 但是只会在可编辑区域触发( keyPress 可以在任意可以获取焦点的地方触发 )
  • DOM2 级的变动事件能够在 DOM 中某一部分发生变化时触发。详情可以参考 MDN 手册:developer.mozilla.org/zh-CN/docs/…
  • hashchange 事件,url 发生任何变化时触发,这是现代单页面应用的基础之一。

5. 事件优化

5.1 事件委托

由于在指定事件处理程序时,需要访问 DOM,严重影响整个页面的交互就绪时间。因此可以通过事件委托,利用事件冒泡,指定一个尽量高层次的节点 DOM 管理一类事件。

5.2 移除无用的事件处理程序

每当事件处理程序指定给元素时,运行中的浏览器代码与支持页面交互的 JavaScript 代码之间就会建立一个连接。连接越多,页面执行就越慢。

6. 模拟事件

  • 利用 js 中原生提供的 Element API 直接出发。如 button.click()
  • 利用 document.createEvent() 创建一个完整的 event 对象,实现更复杂的功能。详情参考:developer.mozilla.org/zh-CN/docs/…
  • 自定义事件:通过 document.createEvent('CustomEvent') 创建。