# 事件

JavaScript 与 HTML 的交互是通过事件实现的,事件代表文档或浏览器窗口中某个有意义的时刻。

# 1、事件流

事件流描述了页面接收事件的顺序。

IE 将支持事件冒泡流,而 Netscape Communicator 将支持事件捕获流。

# 1.1 事件冒泡

IE 事件流被称为事件冒泡,这是因为事件被定义为从最具体的元素(文档树中最深的节点)开始触发,然后向上传播至没有那么具体的元素(文档)。

<!DOCTYPE html> 
<html> 
  <head> 
    <title>Event Bubbling Example</title> 
  </head> 
  <body> 
    <div id="myDiv">Click Me</div> 
  </body> 
</html>
1
2
3
4
5
6
7
8
9

在点击页面中的<div>元素后,click 事件会以如下顺序发生:

  • (1) <div>
  • (2) <body>
  • (3) <html>
  • (4) document

所有现代浏览器都支持事件冒泡,只是在实现方式上会有一些变化。

# 1.2 事件捕获

Netscape Communicator 团队提出了另一种名为事件捕获的事件流。事件捕获的意思是最不具体的节点应该最先收到事件,而最具体的节点应该最后收到事件。事件捕获实际上是为了在事件到达最终目标前拦截事件。如果前面的例子使用事件捕获,则点击<div>元素会以下列顺序触发 click 事件:

  • (1) document
  • (2) <html>
  • (3) <body>
  • (4) <div>

虽然这是 Netscape Communicator 唯一的事件流模型,但事件捕获得到了所有现代浏览器的支持。

实际上,所有浏览器都是从 window 对象开始捕获事件,而 DOM2 Events规范规定的是从 document 开始。

# 1.3 DOM 事件流

DOM2 Events 规范规定事件流分为 3 个阶段:事件捕获到达目标事件冒泡。事件捕获最先发生,为提前拦截事件提供了可能。然后,实际的目标元素接收到事件。最后一个阶段是冒泡,最迟要在这个阶段响应事件。

事件流

在 DOM 事件流中,实际的目标(<div>元素)在捕获阶段不会接收到事件。这是因为捕获阶段从document 到<html>再到<body>就结束了。下一阶段,即会在<div>元素上触发事件的“到达目标”阶段,通常在事件处理时被认为是冒泡阶段的一部分(稍后讨论)。然后,冒泡阶段开始,事件反向传播至文档。

# 2、事件处理程序

事件意味着用户或浏览器执行的某种动作。比如,单击(click)、加载(load)、鼠标悬停(mouseover)。为响应事件而调用的函数被称为事件处理程序(或事件监听器)。事件处理程序的名字以"on"开头,因此 click 事件的处理程序叫作 onclick,而 load 事件的处理程序叫作 onload。

# 2.1 HTML 事件处理程序

<input type="button" value="Click Me" onclick="console.log('Clicked')"/>
1

# 2.2 DOM事件级别

DOM 级别一共可以分为四个级别:DOM0 级、DOM1 级、DOM2 级和 DOM3 级。而 DOM 事件分为 3 个级别:DOM 0 级事件处理,DOM 2 级事件处理和 DOM 3 级事件处理。由于 DOM 1 级中没有事件的相关内容,所以没有DOM 1 级事件。

(1)DOM0 事件处理程序

每个元素(包括 window 和 document)都有通常小写的事件处理程序属性,比如 onclick。只要把这个属性赋值为一个函数即可:

let btn = document.getElementById("myBtn"); 
btn.onclick = function() { 
  console.log("Clicked"); 
  console.log(this.id); // "myBtn" 
}; 
btn.onclick = null; // 移除事件处理程序
1
2
3
4
5
6

(2)DOM2 事件处理程序

DOM2 Events 为事件处理程序的赋值和移除定义了两个方法:addEventListener() 和 removeEventListener()。

这两个方法暴露在所有 DOM 节点上,它们接收 3 个参数:事件名、事件处理函数和一个布尔值,true 表示在捕获阶段调用事件处理程序,false(默认值)表示在冒泡阶段调用事件处理程序。

let btn = document.getElementById("myBtn"); 
btn.addEventListener("click", () => { 
  console.log(this.id); 
}, false);
1
2
3
4

添加了两个事件处理程序。多个事件处理程序以添加顺序来触发。

let btn = document.getElementById("myBtn"); 
btn.addEventListener("click", () => { 
 console.log(this.id); 
 }, false); 
// 其他代码
btn.removeEventListener("click", function() { // 没有效果!
 console.log(this.id); 
}, false); 

// 绑定 handler
let btn = document.getElementById("myBtn"); 
let handler = function() { 
 console.log(this.id); 
}; 
btn.addEventListener("click", handler, false); 
// 其他代码
btn.removeEventListener("click", handler, false); // 有效果!
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17

addEventListener() 和 removeEventListener() 必须是传入同一个函数才有效。

# 3、事件对象

在 DOM 中发生事件时,所有相关信息都会被收集并存储在一个名为 event 的对象中。这个对象包含了一些基本信息,比如导致事件的元素、发生的事件类型,以及可能与特定事件相关的任何其他数据。

DOM 合规的浏览器中,event 对象是传给事件处理程序的唯一参数。

不管以哪种方式(DOM0 或 DOM2)指定事件处理程序,都会传入这个 event 对象。

let btn = document.getElementById("myBtn"); 
btn.onclick = function(event) { 
  console.log(event.type); // "click" 
}; 
btn.addEventListener("click", (event) => { 
  console.log(event.type); // "click" 
}, false);
1
2
3
4
5
6
7

所有事件对象都会包含下表列出的这些公共属性和方法。

属性/方法 类 型 读/写 说 明
target 元素 只读 事件目标
currentTarget 元素 只读 当前事件处理程序所在的元素
preventDefault() 函数 只读 用于取消事件的默认行为。只有 cancelable 为 true 才可以调用这个方法
defaultPrevented 布尔值 只读 true 表示已经调用 preventDefault()方法(DOM3 Events 中新增)
bubbles 布尔值 只读 表示事件是否冒泡
stopPropagation() 函数 只读 用于取消所有后续事件捕获或事件冒泡。只有 bubbles 为 true 才可以调用这个方法
stopImmediatePropagation() 函数 只读 用于取消所有后续事件捕获或事件冒泡,并阻止调用任何后续事件处理程序(DOM3 Events 中新增)
cancelable 布尔值 只读 表示是否可以取消事件的默认行为
detail 整数 只读 事件相关的其他信息
eventPhase 整数 只读 表示调用事件处理程序的阶段:1 代表捕获阶段,2 代表到达目标,3 代表冒泡阶段
trusted 布尔值 只读 true 表示事件是由浏览器生成的。false 表示事件是开发者通过 JavaScript 创建的(DOM3 Events 中新增)
type 字符串 只读 被触发的事件类型
View AbstractView 只读 与事件相关的抽象视图。等于事件所发生的 window 对象

在事件处理程序内部,this 对象始终等于 currentTarget 的值,而 target 只包含事件的实际目标。如果事件处理程序直接添加在了意图的目标,则 this、currentTarget 和 target 的值是一样的。

let btn = document.getElementById("myBtn"); 
btn.onclick = function(event) { 
  console.log(event.currentTarget === this); // true 
  console.log(event.target === this); // true 
}; 
1
2
3
4
5

event 对象只在事件处理程序执行期间存在,一旦执行完毕,就会被销毁。

# 4、事件类型

DOM3 Events 定义了如下事件类型。

  • 用户界面事件(UIEvent):涉及与 BOM 交互的通用浏览器事件。
  • 焦点事件(FocusEvent):在元素获得和失去焦点时触发。
  • 鼠标事件(MouseEvent):使用鼠标在页面上执行某些操作时触发。
  • 滚轮事件(WheelEvent):使用鼠标滚轮(或类似设备)时触发。
  • 输入事件(InputEvent):向文档中输入文本时触发。
  • 键盘事件(KeyboardEvent):使用键盘在页面上执行某些操作时触发。
  • 合成事件(CompositionEvent):在使用某种 IME(Input Method Editor,输入法编辑器)输入字符时触发。

除了这些事件类型之外,HTML5 还定义了另一组事件,而浏览器通常在 DOM 和 BOM 上实现专有事件。这些专有事件基本上都是根据开发者需求而不是按照规范增加的,因此不同浏览器的实现可能不同。

# 4.1 用户界面事件

用户界面事件或 UI 事件不一定跟用户操作有关。这类事件在 DOM 规范出现之前就已经以某种形式存在了,保留它们是为了向后兼容。

  • load:在 window 上当页面加载完成后触发,在窗套(<frameset>)上当所有窗格(<frame>)都加载完成后触发,在<img>元素上当图片加载完成后触发,在<object>元素上当相应对象加载完成后触发。
  • unload:在 window 上当页面完全卸载后触发,在窗套上当所有窗格都卸载完成后触发,在<object>元素上当相应对象卸载完成后触发。
  • abort:在<object>元素上当相应对象加载完成前被用户提前终止下载时触发。
  • error:在 window 上当 JavaScript 报错时触发,在<img>元素上当无法加载指定图片时触发,在<object>元素上当无法加载相应对象时触发,在窗套上当一个或多个窗格无法完成加载时触发。
  • select:在文本框(<input>或 textarea)上当用户选择了一个或多个字符时触发。
  • resize:在 window 或窗格上当窗口或窗格被缩放时触发。
  • scroll:当用户滚动包含滚动条的元素时在元素上触发。<body>元素包含已加载页面的滚动条。

# 4.2 焦点事件

焦点事件在页面元素获得或失去焦点时触发。这些事件可以与 document.hasFocus() 和 document.activeElement 一起为开发者提供用户在页面中导航的信息。

  • blur:当元素失去焦点时触发。这个事件不冒泡,所有浏览器都支持。
  • focus:当元素获得焦点时触发。这个事件不冒泡,所有浏览器都支持。
  • focusin:当元素获得焦点时触发。这个事件是 focus 的冒泡版。
  • focusout:当元素失去焦点时触发。这个事件是 blur 的通用版。

当焦点从页面中的一个元素移到另一个元素上时,会依次发生如下事件。

  • (1) focuscout 在失去焦点的元素上触发。
  • (2) focusin 在获得焦点的元素上触发。
  • (3) blur 在失去焦点的元素上触发。
  • (4) DOMFocusOut 在失去焦点的元素上触发。
  • (5) focus 在获得焦点的元素上触发。
  • (6) DOMFocusIn 在获得焦点的元素上触发。

# 4.3 鼠标和滚轮事件

DOM3 Events 定义了 9 种鼠标事件。

  • click:在用户单击鼠标主键(通常是左键)或按键盘回车键时触发。
  • dblclick:在用户双击鼠标主键(通常是左键)时触发。
  • mousedown:在用户按下任意鼠标键时触发。这个事件不能通过键盘触发。
  • mouseenter:在用户把鼠标光标从元素外部移到元素内部时触发。这个事件不冒泡,也不会在光标经过后代元素时触发。
  • mouseleave:在用户把鼠标光标从元素内部移到元素外部时触发。这个事件不冒泡,也不会在光标经过后代元素时触发。
  • mousemove:在鼠标光标在元素上移动时反复触发。这个事件不能通过键盘触发。
  • mouseout:在用户把鼠标光标从一个元素移到另一个元素上时触发。移到的元素可以是原始元素的外部元素,也可以是原始元素的子元素。这个事件不能通过键盘触发。
  • mouseover:在用户把鼠标光标从元素外部移到元素内部时触发。这个事件不能通过键盘触发。
  • mouseup:在用户释放鼠标键时触发。这个事件不能通过键盘触发。

# 4.4 事件坐标

  • event.clientX 客户端 x 坐标
  • event.clientY 客户端 y 坐标
  • event.pageX 页面 x 坐标
  • event.pageY 页面 y 坐标
  • event.screenX 屏幕 x 坐标
  • event.screenY 屏幕 y 坐标
let div = document.getElementById("myDiv"); 
div.addEventListener("click", (event) => { 
  console.log(`Client coordinates: ${event.clientX}, ${event.clientY}`);
  console.log(`Page coordinates: ${event.pageX}, ${event.pageY}`);
  console.log(`Screen coordinates: ${event.screenX}, ${event.screenY}`);
}); 
1
2
3
4
5
6

# 4.5 修饰键

DOM 规定了 4 个属性来表示这修饰键的状态:shiftKey、ctrlKey、altKey 和 metaKey。

let div = document.getElementById("myDiv"); 
div.addEventListener("click", (event) => { 
  let keys = new Array(); 
  if (event.shiftKey) { 
    console.log("shift");
  } 
  if (event.ctrlKey) {  
    console.log("ctrl");
  } 
  if (event.altKey) { 
    console.log("alt");
  } 
  if (event.metaKey) { 
    console.log("meta");
  } 
});
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16

# 4.6 鼠标按键

  • 0,表示没有按下任何键;
  • 1,表示按下鼠标主键;
  • 2,表示按下鼠标副键;
  • 3,表示同时按下鼠标主键、副键;
  • 4,表示按下鼠标中键;
  • 5,表示同时按下鼠标主键和中键;
  • 6,表示同时按下鼠标副键和中键;
  • 7,表示同时按下 3 个键。

# 4.7 键盘与输入事件

键盘事件包含 3 个事件:

  • keydown,用户按下键盘上某个键时触发,而且持续按住会重复触发。
  • keyup,用户释放键盘上某个键时触发。
  • textInput,在字符被输入到可编辑区域时触发。
let textbox = document.getElementById("myText"); 
textbox.addEventListener("keyup", (event) => { 
  console.log(event.keyCode); 
}); 
1
2
3
4

# 4.8 合成事件

合成事件是 DOM3 Events 中新增的,用于处理通常使用 IME 输入时的复杂输入序列。IME 可以让用户输入物理键盘上没有的字符。

IME 通常需要同时按下多个键才能输入一个字符。合成事件用于检测和控制这种输入。

合成事件有以下 3 种:

  • compositionstart,在 IME 的文本合成系统打开时触发,表示输入即将开始;
  • compositionupdate,在新字符插入输入字段时触发;
  • compositionend,在 IME 的文本合成系统关闭时触发,表示恢复正常键盘输入。

# 4.9 变化事件

DOM2 的变化事件(Mutation Events)是为了在 DOM 发生变化时提供通知。

变化事件已经被Mutation Observers 所取代,可以参考 MutationObserver 接口

# 4.10 HTML5 事件

  • contextmenu 事件,可以处理页面上的所有同类事件。
  • beforeunload 事件,在页面即将从浏览器中卸载时触发
  • DOMContentLoaded 事件,在 DOM 树构建完成后立即触发,而不用等待图片、JavaScript 文件、CSS 文件或其他资源加载完成。

# 4.11 设备事件

设备事件可以用于确定用户使用设备的方式。

(1)orientationchange 事件

判断用户的设备是处于垂直模式还是水平模式。

移动 Safari 在 window 上暴露了 window.orientation 属性,它有以下 3 种值之一:0 表示垂直模式,90 表示左转水平模式(主屏幕键在右侧),–90 表示右转水平模式(主屏幕键在左)。

所有 iOS 设备都支持 orientationchange 事件和 window.orientation 属性。

(2)deviceorientation 事件

可以获取设备的加速计信息,而且数据发生了变化,这个事件就会在 window 上触发。

设备本身处于 3D 空间即拥有 x 轴、y 轴和 z 轴的坐标系中。如果把设备静止放在水平的表面上,那么三轴的值均为 0,其中,x 轴方向为从设备左侧到右侧,y 轴方向为从设备底部到上部,z 轴方向为从设备背面到正面

当 deviceorientation 触发时,event 对象中会包含各个轴相对于设备静置时坐标值的变化,主要是以下 5 个属性。

  • alpha:0~360 范围内的浮点值,表示围绕 z 轴旋转时 y 轴的度数(左右转)。
  • beta:–180~180 范围内的浮点值,表示围绕 x 轴旋转时 z 轴的度数(前后转)。
  • gamma:–90~90 范围内的浮点值,表示围绕 y 轴旋转时 z 轴的度数(扭转)。
  • absolute:布尔值,表示设备是否返回绝对值。
  • compassCalibrated:布尔值,表示设备的指南针是否正确校准。

(3)devicemotion 事件

用于提示设备实际上在移动,而不仅仅是改变了朝向。

  • acceleration:对象,包含 x、y 和 z 属性,反映不考虑重力情况下各个维度的加速信息。
  • accelerationIncludingGravity:对象,包含 x、y 和 z 属性,反映各个维度的加速信息,包含 z 轴自然重力加速度。
  • interval:毫秒,距离下次触发 devicemotion 事件的时间。此值在事件之间应为常量。
  • rotationRate:对象,包含 alpha、beta 和 gamma 属性,表示设备朝向。

# 4.12 触摸及手势事件

Safari 为 iOS 定制了一些专有事件,以方便开发者。因为 iOS 设备没有鼠标和键盘,所以常规的鼠标和键盘事件不足以创建具有完整交互能力的网页。同时,WebKit 也为 Android 定制了很多专有事件,成为了事实标准,并被纳入 W3C 的 Touch Events 规范。

触摸事件有如下几种。

  • touchstart:手指放到屏幕上时触发(即使有一个手指已经放在了屏幕上)。
  • touchmove:手指在屏幕上滑动时连续触发。在这个事件中调用 preventDefault()可以阻止滚动。
  • touchend:手指从屏幕上移开时触发。
  • touchcancel:系统停止跟踪触摸时触发。文档中并未明确什么情况下停止跟踪。

这些事件都会冒泡,也都可以被取消。

手势事件:在两个手指触碰屏幕且相对距离或旋转角度变化时触发。

  • gesturestart:一个手指已经放在屏幕上,再把另一个手指放到屏幕上时触发。
  • gesturechange:任何一个手指在屏幕上的位置发生变化时触发。
  • gestureend:其中一个手指离开屏幕时触发。

# 5、内存与性能

“过多事件处理程序”的解决方案是使用事件委托。事件委托利用事件冒泡,可以只使用一个事件处理程序来管理一种类型的事件。

比如有以下 HTML:

<ul id="myLinks"> 
  <li id="goSomewhere">Go somewhere</li> 
  <li id="doSomething">Do something</li> 
  <li id="sayHi">Say hi</li> 
</ul>
1
2
3
4
5

绑定事件:

let item1 = document.getElementById("goSomewhere"); 
let item2 = document.getElementById("doSomething"); 
let item3 = document.getElementById("sayHi"); 
item1.addEventListener("click", (event) => { 
  location.href = "http:// www.wrox.com"; 
}); 
item2.addEventListener("click", (event) => { 
  document.title = "I changed the document's title"; 
}); 
item3.addEventListener("click", (event) => { 
  console.log("hi"); 
});

// 修改过后
let list = document.getElementById("myLinks"); 
list.addEventListener("click", (event) => { 
  let target = event.target; 
  switch(target.id) { 
    case "doSomething": 
      document.title = "I changed the document's title"; 
      break; 
    case "goSomewhere": 
      location.href = "http:// www.wrox.com"; 
      break; 
    case "sayHi": 
      console.log("hi"); 
      break; 
 } 
});
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29

最适合使用事件委托的事件包括:click、mousedown、mouseup、keydown 和 keypress。

# 6、模拟事件

# 6.1 DOM 事件模拟

任何时候,都可以使用 document.createEvent() 方法创建一个 event 对象。这个方法接收一个参数,此参数是一个表示要创建事件类型的字符串。

  • "UIEvents"(DOM3 中是"UIEvent"):通用用户界面事件(鼠标事件和键盘事件都继承自这个事件)。
  • "MouseEvents"(DOM3 中是"MouseEvent"):通用鼠标事件。
  • "HTMLEvents"(DOM3 中没有):通用 HTML 事件(HTML 事件已经分散到了其他事件大类中)。

创建 event 对象之后,需要使用事件相关的信息来初始化。事件模拟的最后一步是触发事件。为此要使用 dispatchEvent() 方法

(1)模拟鼠标事件

initMouseEvent() 方法,用于为新对象指定鼠标的特定信息。

initMouseEvent() 方法接收 15 个参数,分别对应鼠标事件会暴露的属性。这些参数列举如下。

  • type(字符串):要触发的事件类型,如"click"。
  • bubbles(布尔值):表示事件是否冒泡。为精确模拟鼠标事件,应该设置为 true。
  • cancelable(布尔值):表示事件是否可以取消。为精确模拟鼠标事件,应该设置为 true。
  • view(AbstractView):与事件关联的视图。基本上始终是 document.defaultView。
  • detail(整数):关于事件的额外信息。只被事件处理程序使用,通常为 0。
  • screenX(整数):事件相对于屏幕的 x 坐标。
  • screenY(整数):事件相对于屏幕的 y 坐标。
  • clientX(整数):事件相对于视口的 x 坐标。
  • clientY(整数):事件相对于视口的 y 坐标。
  • ctrlkey(布尔值):表示是否按下了 Ctrl 键。默认为 false。
  • altkey(布尔值):表示是否按下了 Alt 键。默认为 false。
  • shiftkey(布尔值):表示是否按下了 Shift 键。默认为 false。
  • metakey(布尔值):表示是否按下了 Meta 键。默认为 false。
  • button(整数):表示按下了哪个按钮。默认为 0。
  • relatedTarget(对象):与事件相关的对象。只在模拟 mouseover 和 mouseout 时使用。

模拟单击:

let btn = document.getElementById("myBtn"); 
// 创建 event 对象
let event = document.createEvent("MouseEvents"); 
// 初始化 event 对象
event.initMouseEvent("click", true, true, document.defaultView, 
 0, 0, 0, 0, 0, false, false, false, false, 0, null); 
// 触发事件
btn.dispatchEvent(event);
1
2
3
4
5
6
7
8

(2)模拟键盘事件

在 DOM3 中创建键盘事件的方式是给 createEvent() 方法传入参数"KeyboardEvent"。这样会返回一个 event 对象,这个对象有一个 initKeyboardEvent() 方法。这个方法接收以下参数。

  • type(字符串):要触发的事件类型,如"keydown"。
  • bubbles(布尔值):表示事件是否冒泡。为精确模拟键盘事件,应该设置为 true。
  • cancelable(布尔值):表示事件是否可以取消。为精确模拟键盘事件,应该设置为 true。
  • view(AbstractView):与事件关联的视图。基本上始终是 document.defaultView。
  • key(字符串):按下按键的字符串代码。
  • location(整数):按下按键的位置。0 表示默认键,1 表示左边,2 表示右边,3 表示数字键盘,4 表示移动设备(虚拟键盘),5 表示游戏手柄。
  • modifiers(字符串):空格分隔的修饰键列表,如"Shift"。
  • repeat(整数):连续按了这个键多少次。

注意,DOM3 Events 废弃了 keypress 事件,因此只能通过上述方式模拟 keydown 和 keyup 事件

let textbox = document.getElementById("myTextbox"), 
 event; 
// 按照 DOM3 的方式创建 event 对象
if (document.implementation.hasFeature("KeyboardEvents", "3.0")) { 
 event = document.createEvent("KeyboardEvent"); 
 // 初始化 event 对象
 event.initKeyboardEvent("keydown", true, true, document.defaultView, "a", 
 0, "Shift", 0); 
} 
// 触发事件
textbox.dispatchEvent(event);
1
2
3
4
5
6
7
8
9
10
11

(3)模拟其他事件

let event = document.createEvent("HTMLEvents"); 
event.initEvent("focus", true, false); 
target.dispatchEvent(event); 
1
2
3

(4)自定义 DOM 事件

要创建自定义事件,需要调用 createEvent("CustomEvent") 。返回的对象包含 initCustomEvent() 方法,该方法接收以下 4 个参数。

  • type(字符串):要触发的事件类型,如"myevent"。
  • bubbles(布尔值):表示事件是否冒泡。
  • cancelable(布尔值):表示事件是否可以取消。
  • detail(对象):任意值。作为 event 对象的 detail 属性。
let div = document.getElementById("myDiv"), 
 event; 
div.addEventListener("myevent", (event) => { 
 console.log("DIV: " + event.detail); 
}); 
document.addEventListener("myevent", (event) => { 
 console.log("DOCUMENT: " + event.detail); 
}); 
if (document.implementation.hasFeature("CustomEvents", "3.0")) { 
 event = document.createEvent("CustomEvent"); 
 event.initCustomEvent("myevent", true, false, "Hello world!"); 
 div.dispatchEvent(event); 
} 
1
2
3
4
5
6
7
8
9
10
11
12
13
上次更新: 3/9/2022, 6:56:39 PM