Skip to main content

[JS] 事件

摘记<Javascript高级程序设计> -- 马特 2020版 第十七章

事件

JavaScript 与 HTML 的交互是通过事件实现的,事件代表文档或浏览器窗口中某个有意义的时刻。可以使用仅在事件发生时执行的监听器(也叫处理程序)订阅事件。在传统软件工程领域,这个模型叫“观察者模式”,其能够做到页面行为(在 JavaScript 中定义)与页面展示(在 HTML 和 CSS 中定义)的分离。

事件流

在第四代 Web 浏览器(IE4 和 Netscape Communicator 4)开始开发时,开发团队碰到了一个有意思的问题:页面哪个部分拥有特定的事件呢?要理解这个问题,可以在一张纸上画几个同心圆。把手指放到圆心上,则手指不仅是在一个圆圈里,而且是在所有的圆圈里。两家浏览器的开发团队都是以同样的方式看待浏览器事件的。当你点击一个按钮时,实际上不光点击了这个按钮,还点击了它的容器以及整个页面。

事件流描述了页面接收事件的顺序。结果非常有意思,IE 和 Netscape 开发团队提出了几乎完全相反的事件流方案。IE 将支持事件冒泡流,而 Netscape Communicator 将支持事件捕获流。

事件冒泡

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

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

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

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

事件捕获

Netscape Communicator 团队提出了另一种名为事件捕获的事件流。事件捕获的意思是最不具体的节点应该最先收到事件,而最具体的节点应该最后收到事件。事件捕获实际上是为了在事件到达最终目标前拦截事件。

事件处理程序

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

HTML 事件处理程序

特定元素支持的每个事件都可以使用事件处理程序的名字以 HTML 属性的形式来指定。此时属性的值必须是能够执行的 JavaScript 代码。例如,要在按钮被点击时执行某些 JavaScript 代码,可以使用以下 HTML 属性:

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

在 HTML 中定义的事件处理程序可以包含精确的动作指令,也可以调用在页面其他地方定义的脚本,比如:

<script>
function showMessage() {
console.log("Hello world!");
}
</script>
<input type="button" value="Click Me" onclick="showMessage()"/>

DOM0 事件处理程序

在 JavaScript 中指定事件处理程序的传统方式是把一个函数赋值给(DOM 元素的)一个事件处理程序属性。这也是在第四代 Web 浏览器中开始支持的事件处理程序赋值方法,直到现在所有现代浏览器仍然都支持此方法,主要原因是简单。要使用 JavaScript 指定事件处理程序,必须先取得要操作对象的引用。

DOM2 事件处理程序

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

跨浏览器事件处理程序

为了以跨浏览器兼容的方式处理事件,很多开发者会选择使用一个 JavaScript 库,其中抽象了不同浏览器的差异。有些开发者也可能会自己编写代码,以便使用最合适的事件处理手段。自己编写跨浏览器事件处理代码也很简单,主要依赖能力检测。要确保事件处理代码具有最大兼容性,只需要让代码在冒泡阶段运行即可。

内存与性能

因为事件处理程序在现代 Web 应用中可以实现交互,所以很多开发者会错误地在页面中大量使用它们。在创建 GUI 的语言如 C#中,通常会给 GUI 上的每个按钮设置一个 onclick 事件处理程序。这样做不会有什么性能损耗。在 JavaScript 中,页面中事件处理程序的数量与页面整体性能直接相关。原因有很多。首先,每个函数都是对象,都占用内存空间,对象越多,性能越差。其次,为指定事件处理程序所需访问 DOM 的次数会先期造成整个页面交互的延迟。只要在使用事件处理程序时多注意一些方法,就可以改善页面性能。

事件委托

“过多事件处理程序”的解决方案是使用事件委托。事件委托利用事件冒泡,可以只使用一个事件处理程序来管理一种类型的事件。例如,click 事件冒泡到 document。这意味着可以为整个页面指定一个 onclick 事件处理程序,而不用为每个可点击元素分别指定事件处理程序。比如有以下 HTML:

<ul id="myLinks">
<li id="goSomewhere">Go somewhere</li>
<li id="doSomething">Do something</li>
<li id="sayHi">Say hi</li>
</ul>

这里的 HTML 包含 3 个列表项,在被点击时应该执行某个操作。对此,通常的做法是像这样指定 3个事件处理程序:

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");
});

如果对页面中所有需要使用 onclick 事件处理程序的元素都如法炮制,结果就会出现大片雷同的只为指定事件处理程序的代码。使用事件委托,只要给所有元素共同的祖先节点添加一个事件处理程序,就可以解决问题。比如:

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;
}
});

这里只给<ul id="myLinks">元素添加了一个 onclick 事件处理程序。因为所有列表项都是这个元素的后代,所以它们的事件会向上冒泡,最终都会由这个函数来处理。但事件目标是每个被点击的列表项,只要检查 event 对象的 id 属性就可以确定,然后再执行相应的操作即可。相对于前面不使用事件委托的代码,这里的代码不会导致先期延迟,因为只访问了一个 DOM 元素和添加了一个事件处理程序。结果对用户来说没有区别,但这种方式占用内存更少。所有使用按钮的事件(大多数鼠标事件和键盘事件)都适用于这个解决方案。

只要可行,就应该考虑只给 document 添加一个事件处理程序,通过它处理页面中所有某种类型的事件。相对于之前的技术,事件委托具有如下优点。

  • document 对象随时可用,任何时候都可以给它添加事件处理程序(不用等待 DOMContentLoaded或 load 事件)。这意味着只要页面渲染出可点击的元素,就可以无延迟地起作用
  • 节省花在设置页面事件处理程序上的时间。只指定一个事件处理程序既可以节省 DOM 引用,也可以节省时间。
  • 减少整个页面所需的内存,提升整体性能。

最适合使用事件委托的事件包括:

  • click、
  • mousedown、
  • mouseup、
  • keydown
  • keypress。