带有用户界面的应用程序——以及其他类型的应用程序——主要由事件驱动。在这里,我们专注于 DOM 事件。它们来自于浏览器或本地应用程序中的特定操作或情况,例如,用户点击鼠标、按下键盘键、或在触摸屏上操作;视觉对象被“拖放”、“复制粘贴”或调整大小;HTML 页面加载或卸载;浏览器或视觉对象获取或失去焦点等。应用程序还可以通过编程方式创建事件(调度)。

事件与其源对象相关,并与 JavaScript 语句关联;通常,这是对一个函数的调用,该函数被称为事件处理程序。JavaScript 部分在事件发生后被调用。常见的操作包括与服务器的通信、用户输入的验证、或修改 DOM 或图形。

创建和触发事件

嵌入在 HTML 中

一个简短的示例展示了如何在 HTML 页面中定义事件、触发事件并执行操作。这种语法版本被称为内联 JavaScript。

<!DOCTYPE html>
<html>
<head>
  <script>
  function handleEvent(evt) {
    "use strict";
    alert("通过 JavaScript 函数执行一些操作。");
  }
  </script>
</head>

<body id="body" style="margin:2em">
  <!-- 内联所有语句;不常见 -->
  <button onclick="const msg='直接调用小操作。'; alert(msg);">第一个按钮</button>
  <!-- 调用函数(事件处理程序) -->
  <button onclick="handleEvent(event)">第二个按钮</button>
</body>
</html>

当用户点击其中一个按钮时,浏览器会读取按钮的 onclick 属性,创建一个包含事件所有属性的 JavaScript 对象,并执行相应的 JavaScript 语句。通常,这部分 JavaScript 代码是对一个函数的调用,这个函数被称为事件处理程序。该函数接收 JavaScript 对象作为其第一个参数。只有在非常简单的情况下,完整的 JavaScript 脚本才会内联。

当然,调用的函数必须在某处存在。一些标准函数,如 alert(),是由浏览器预定义并提供的。你自己编写的函数存在于 HTML 标签 <script> 中,或者该标签引用一个文件或 URL,指向函数所在的地方。

提示:将事件定义直接嵌入 HTML 中被称为内联 JavaScript。这是注册事件处理程序的最早方法,但它倾向于使源代码在非简单应用中难以阅读。因此,它可以被认为比其他非干扰性技术(见下一章)不太理想。内联 JavaScript 的使用与使用内联 CSS 类似,即通过将 CSS 放入 style 属性中来为 HTML 添加样式。

尽管如此,本书将在演示页面中经常使用内联 JavaScript,因为语法简洁,概念易于理解。

通过 JavaScript 编程

JavaScript 有两种方式来为 HTML 元素注册事件处理程序。首先,事件处理函数可以直接赋值给元素的属性 onxxx(如 onclick, onkeydown, onload, onfocus 等)。它们的名称以 on 开头,后跟事件类型的值。其次,可以使用 addEventListener 函数来注册事件类型和事件处理程序。

<!DOCTYPE html>
<html>
<head>
  <script>
  function register() {
    "use strict";
    const p1 = document.getElementById("p1");

    // 这样做...
    p1.onclick = showMessage; // 不加括号
    // 或者这样做(推荐)
    // p1.addEventListener('click', showMessage);
    alert("事件处理程序已分配给段落。");
  }
  
  function showMessage(evt) {
    "use strict";
    // 参数 'evt' 包含关于事件的许多信息,
    // 例如,它起源的位置信息
    alert("点击事件到段落。位置为 " + evt.clientX + " / " + evt.clientY + "。事件类型是:" + evt.type);
  }
  </script>
</head>

<body>
  <h1>注册事件</h1>
  <p id="p1" style="margin:2em; background-color:aqua">
     一个小段落。先没有事件处理程序,然后有一个事件处理程序。
  </p>
  <button onclick="register()" id="button_1">一个按钮</button>
</body>
</html>

按钮 button_1onclick 事件处理程序 register 通过上述内联 JavaScript 语法注册。当页面加载时,仅知道这个事件处理程序。点击段落 p1 不会触发任何操作,因为它尚未有事件处理程序。但是,当按钮被按下时,按钮 button_1 的事件处理程序会注册第二个事件处理程序,即函数 showMessage。现在,在段落上点击后,会弹出 "点击事件到段落..." 的提示框。

注册发生在第 10 行:p1.onclick = showMessage。与内联 JavaScript 的显著区别在于这里没有使用括号。内联 JavaScript 会调用函数 showMessage,因此需要使用括号。而 register 函数并没有调用 showMessage,它只是使用了函数名来完成注册过程。

使用 addEventListener 函数是为段落注册事件的另一种方法。它作用于元素 p1 并接受两个参数。第一个参数是事件类型(如 click, keydown 等),这些事件类型与 onxxx 名称相关,只是缺少前两个字符 on。第二个参数是作为事件处理程序的函数名称。

你可以通过注释掉第 10 行或第 12 行来测试这个例子。第 12 行使用的 addEventListener 是推荐的版本。

事件类型

根据原始元素的类型,存在不同种类的事件。完整的事件类型列表非常庞大(参考 MDN 和 W3Schools)。我们将展示一些重要的事件类型和示例。

名称 描述
blur 输入元素失去焦点
change 元素被修改
click 元素被点击
dblclick 元素被双击
error 加载元素时发生错误
focus 输入元素获得焦点
keydown 在元素获得焦点时按下键
keyup 在元素获得焦点时释放键
load 元素已加载
mouseenter 鼠标指针移入元素
mousemove 鼠标指针在元素内移动
mousedown 鼠标按钮按下
mouseup 鼠标按钮释放
mouseleave 鼠标指针移出元素
reset 表单的重置按钮被点击
resize 包含的窗口或框架被调整大小
select 在元素中选择了一些文本
submit 表单正在提交
unload 内容正在卸载(例如,窗口关闭)

以下示例演示了几种不同的事件类型:

<!DOCTYPE html>
<html>
<head>
  <script>
  function registerAllEvents() {
    "use strict";
    // 注册不同的事件类型
    document.getElementById("p1").addEventListener("click", handleAllEvents);
    document.getElementById("p2").addEventListener("dblclick", handleAllEvents);
    document.getElementById("p3").addEventListener("mouseover", handleAllEvents);
    document.getElementById("t1").addEventListener("keydown", handleAllEvents);
    document.getElementById("t2").addEventListener("select", handleAllEvents);
    document.getElementById("button_1").addEventListener("mouseover", handleAllEvents);
  }
  
  function handleAllEvents(evt) {
    "use strict";
    alert("来自元素的事件发生: " +
          evt.target.id + ". 事件类型是: " + evt.type);
  }
  </script>
</head>

<body onload="registerAllEvents()" style="margin:1em">
  <h1 id="h1" style="background-color:aqua">检查事件</h1>
  <p  id="p1" style="background-color:aqua">一个小段落。(点击)</p>
  <p  id="p2" style="background-color:aqua">一个小段落。(双击)</p>
  <p  id="p3" style="background-color:aqua">一个小段落。(鼠标悬停)</p>
  <p  style="background-color:aqua">
    <textarea id="t1" rows="1" cols="50">(按键)</textarea>
  </p>
  <p  style="background-color:aqua">
    <textarea id="t2" rows="1" cols="50">(选择)</textarea>
  </p>
  <button id="button_1">一个按钮(鼠标悬停)</button>
</body>
</html>

当页面加载时,bodyonload 事件被触发。请注意,在这里 on 前缀是必要的,因为它是内联 JavaScript 语法(第 23 行)。调用的函数 registerAllEvents 定位了不同的 HTML 元素,并注册了不同类型的事件处理程序(第 8 - 13 行)。通常,你会注册不同的函数,但为了简化,我们在这个示例中将相同的函数 handleAllEvents 注册到所有元素上。这个函数会报告事件类型和触发事件的 HTML 元素。

事件属性

事件总是作为其第一个参数以 JavaScript 对象的形式传递给事件处理程序。JavaScript 对象由属性组成,属性是键值对。在所有情况下,其中一个键是 type,它包含事件的类型;一些可能的值已在上述表格中显示。根据事件类型,许多其他属性也会变得可用。

以下是一些重要或常见的属性:

名称 描述
button 返回被点击的鼠标按钮
clientX 返回鼠标指针在本地坐标中的水平坐标(不包括滚动部分)
clientY 返回鼠标指针在本地坐标中的垂直坐标(不包括滚动部分)
code 返回按下的键的文本表示,如 "ShiftLeft" 或 "KeyE"
key 返回按下键的值,如 "E"
offsetX 返回鼠标指针在目标 DOM 元素中的水平坐标
offsetY 返回鼠标指针在目标 DOM 元素中的垂直坐标
pageX 返回鼠标指针在页面坐标中的水平坐标(包括滚动部分)
pageY 返回鼠标指针在页面坐标中的垂直坐标(包括滚动部分)
screenX 返回鼠标指针在完整显示器坐标中的水平坐标
screenY 返回鼠标指针在完整显示器坐标中的垂直坐标
target 返回触发事件的元素
timeStamp 返回元素创建与事件创建之间的毫秒数
type 返回触发事件的元素的类型
x clientX 的别名
y clientY 的别名

访问属性示例

以下脚本示例展示了如何访问事件的属性:

<!DOCTYPE html>
<html>
<head>
  <script>
  function changeTitle(evt) {
    "use strict";
    const xPos = evt.x;
    const yPos = evt.y;
    document.title = [xPos, yPos];
  }
  </script>
</head>

<body onmousemove="changeTitle(event)" style="margin:1em; border-width: 1px; border-style: solid;">
  <h1 id="h1" style="background-color:aqua">检查事件</h1>
  <p id="p1" style="margin:2em; background-color:aqua">一个小段落。</p>
  <p id="p2" style="margin:2em; background-color:aqua">另一个小段落。</p>
  <button id="button_1">按钮</button>
</body>
</html>

这里给 body 注册了一个 mousemove 事件。每当鼠标在页面中移动时,事件会被触发。事件处理程序从 JavaScript 对象中读取 xy 属性,并将其显示在浏览器标签页的标题中。

removeEventListener

addEventListener 类似,removeEventListener 函数可以从元素中移除事件监听器。

合成事件

系统提供了上述展示的丰富事件类型。此外,你还可以创建自己的事件并从应用程序中触发它们。

首先,你需要创建一个包含事件处理程序的函数。接着,使用 addEventListener 将该事件处理程序注册到某个元素上。到目前为止,这与使用预定义的事件类型是相同的。

function register() {
  "use strict";
  
  // ...

  // 选择一个任意的事件类型('addEventListener' 的第一个参数)
  // 并在元素上注册函数
  document.getElementById("p1").addEventListener("syntheticEvent", f);
}

// 非系统事件的事件处理程序
function f(evt) {
  alert("合成事件在元素 " + evt.target.id + " 上被调用,事件类型是: " + evt.type);
}

现在,你可以在应用程序的任何部分触发此事件。为此,你需要创建一个新事件,指定正确的事件类型,并通过 dispatchEvent 方法触发它。

function triggerEvent(evt) {
  "use strict";
  // 创建一个新的事件,指定适当的类型
  const newEvent = new Event("syntheticEvent");
  // 在元素 'p1' 上触发此事件
  document.getElementById("p1").dispatchEvent(newEvent);
}

为测试目的,我们将此功能绑定到按钮上。完整页面如下所示:

<!DOCTYPE html>
<html>
<head>
  <script>
  function register() {
    "use strict";
    document.getElementById("p1").addEventListener("click", showMessage);
    document.getElementById("p2").addEventListener("click", showMessage);
    document.getElementById("button_1").addEventListener("click", triggerEvent);

    // 选择一个任意的事件类型('addEventListener' 的第一个参数)
    // 并在元素上注册函数
    document.getElementById("p1").addEventListener("syntheticEvent", f);
  }

  // 非系统事件的事件处理程序
  function f(evt) {
    "use strict";
    alert("合成事件在元素 " + evt.target.id + " 上被调用,事件类型是: " + evt.type);
  }

  function showMessage(evt) {
    "use strict";
    // 'evt' 参数包含关于事件的许多信息,
    // 例如,它的发生位置
    alert("点击事件发生在元素: " + evt.target.id + ",事件类型是: " + evt.type);
  }

  function triggerEvent(evt) {
    "use strict";
    // 创建一个新的事件,指定适当的类型
    const newEvent = new Event("syntheticEvent");
    // 在元素 'p1' 上触发此事件
    document.getElementById("p1").dispatchEvent(newEvent);
  }

  </script>
</head>

<body onload="register()">
  <h1>以编程方式创建事件。</h1>
  <p id="p1" style="margin:2em; background-color:aqua">一个小段落。</p>
  <p id="p2" style="margin:2em; background-color:aqua">另一个小段落。</p>
  <button id="button_1">按钮</button>
</body>
</html>

在开始时,按钮监听 click 事件,而 p1 既监听 click 类型的事件也监听 syntheticEvent 类型的事件。当点击按钮时,它的事件处理程序 triggerEvent 创建了一个新的 syntheticEvent 类型事件,并在 p1 上触发它(这是此示例的主要目的)。事件处理程序 showMessage 显示一条信息,而无需点击 p1。换句话说:p1 上的事件在没有点击 p1 的情况下被触发。

使用 CustomEvent 传递数据

如果你需要在事件处理程序中获取一些来自调用函数的数据(例如,错误信息文本、HTTP 响应数据等),你可以通过使用 CustomEvent 和其 detail 属性来传递这些数据:

const newEvent = new CustomEvent("syntheticEvent", {detail: "一条简短的消息。"});

function f(evt) {
  "use strict";
  alert("合成事件在元素 " + evt.target.id + " 上被调用,事件类型是: " + evt.type + ". " + evt.detail);
}

(A)同步行为

大多数事件是同步处理的,例如 clickkeymouse 事件。但也有一些例外是异步处理的,例如 load 事件。同步 意味着调用顺序与它们创建的顺序相同。例如,按顺序点击按钮 A、B、C 将依次触发 A、B、C 的事件处理程序。相对而言,异步 事件可能会导致相关处理程序以任意顺序被调用。

需要区分的是,事件处理程序的调用顺序与其实现的方式。每种实现都可以是严格顺序的,也可以包含异步调用——这取决于你的意图。典型的异步调用包括 HTTP 请求或数据库查询。当然,这些也可以是点击事件处理程序的一部分。

Last modified: Monday, 13 January 2025, 3:11 PM