## 十五、处理事件

> 原文：[Handling Events](https://eloquentjavascript.net/15_event.html)
> 
> 译者：[飞龙](https://github.com/wizardforcel)
> 
> 协议：[CC BY-NC-SA 4.0](http://creativecommons.org/licenses/by-nc-sa/4.0/)
> 
> 自豪地采用[谷歌翻译](https://translate.google.cn/)
> 
> 部分参考了[《JavaScript 编程精解（第 2 版）》](https://book.douban.com/subject/26707144/)

> 你对你的大脑拥有控制权，而不是外部事件。认识到这一点，你就找到了力量。
> 
> 马可·奥勒留，《沉思录》

![](img/15-0.jpg)

有些程序处理用户的直接输入，比如鼠标和键盘动作。这种输入方式不是组织整齐的数据结构 - 它是一次一个地，实时地出现的，并且期望程序在发生时作出响应。

## 事件处理器

想象一下，有一个接口，若想知道键盘上是否有一个键是否被按下，唯一的方法是读取那个按键的当前状态。为了能够响应按键动作，你需要不断读取键盘状态，以在按键被释放之前捕捉到按下状态。这种方法在执行时间密集计算时非常危险，因为你可能错过按键事件。

一些原始机器可以像那样处理输入。有一种更进一步的方法，硬件或操作系统发现按键时间并将其放入队列中。程序可以周期性地检查队列，等待新事件并在发现事件时进行响应。

当然，程序必须记得监视队列，并经常做这种事，因为任何时候，按键被按下和程序发现事件之间都会使得软件反应迟钝。该方法被称为轮询。大多数程序员更希望避免这种方法。

一个更好的机制是，系统在发生事件时主动通知我们的代码。浏览器实现了这种特性，支持我们将函数注册为特定事件的处理器。

```html
<p>Click this document to activate the handler.</p>
<script>
  window.addEventListener("click", () => {
    console.log("You knocked?");
  });
</script>
```

`window`绑定指向浏览器提供的内置对象。 它代表包含文档的浏览器窗口。 调用它的`addEventListener`方法注册第二个参数，以便在第一个参数描述的事件发生时调用它。

## 事件与 DOM 节点

每个浏览器事件处理器被注册在上下文中。在为整个窗口注册处理器之前，我们在`window`对象上调用了`addEventListener`。 这种方法也可以在 DOM 元素和一些其他类型的对象上找到。 仅当事件发生在其注册对象的上下文中时，才调用事件监听器。

```html
<button>Click me</button>
<p>No handler here.</p>
<script>
  let button = document.querySelector("button");
  button.addEventListener("click", () => {
    console.log("Button clicked.");
  });
</script>
```

示例代码中将处理器附加到按钮节点上。因此，点击按钮时会触发并执行处理器，而点击文档的其他部分则没有反应。

向节点提供`onclick`属性也有类似效果。这适用于大多数类型的事件 - 您可以为属性附加处理器，属性名称为前面带有`on`的事件名称。

但是一个节点只能有一个`onclick`属性，所以你只能用这种方式为每个节点注册一个处理器。 `addEventListener`方法允许您添加任意数量的处理器，因此即使元素上已经存在另一个处理器，添加处理器也是安全的。

`removeEventListener`方法将删除一个处理器，使用类似于`addEventListener`的参数调用。

```html
<button>Act-once button</button>
<script>
  let button = document.querySelector("button");
  function once() {
    console.log("Done.");
    button.removeEventListener("click", once);
  }
  button.addEventListener("click", once);
</script>
```

赋予`removeEventListener`的函数必须是赋予`addEventListener`的完全相同的函数值。 因此，要注销一个处理其，您需要为该函数提供一个名称（在本例中为`once`），以便能够将相同的函数值传递给这两个方法。

## 事件对象

虽然目前为止我们忽略了它，事件处理器函数作为对象传递：事件（Event）对象。这个对象持有事件的额外信息。例如，如果我们想知道哪个鼠标按键被按下，我们可以查看事件对象的which属性。

```html
<button>Click me any way you want</button>
<script>
  let button = document.querySelector("button");
  button.addEventListener("mousedown", event => {
    if (event.button == 0) {
      console.log("Left button");
    } else if (event.button == 1) {
      console.log("Middle button");
    } else if (event.button == 2) {
      console.log("Right button");
    }
  });
</script>
```

存储在各种类型事件对象中的信息是有差别的。随后本章将会讨论许多类型的事件。对象的`type`属性一般持有一个字符串，表示事件（例如`"click"`和`"mousedown"`）。

## 传播

对于大多数事件类型，在具有子节点的节点上注册的处理器，也将接收发生在子节点中的事件。若点击一个段落中的按钮，段落的事件处理器也会收到点击事件。

但若段落和按钮都有事件处理器，则先执行最特殊的事件处理器（按钮的事件处理器）。也就是说事件向外传播，从触发事件的节点到其父节点，最后直到文档根节点。最后，当某个特定节点上注册的所有事件处理器按其顺序全部执行完毕后，窗口对象的事件处理器才有机会响应事件。

事件处理器任何时候都可以调用事件对象的`stopPropagation`方法，阻止事件进一步传播。该方法有时很实用，例如，你将一个按钮放在另一个可点击元素中，但你不希望点击该按钮会激活外部元素的点击行为。

下面的示例代码将`mousedown`处理器注册到按钮和其外部的段落节点上。在按钮上点击鼠标右键，按钮的处理器会调用`stopPropagation`，调度段落上的事件处理器执行。当点击鼠标其他键时，两个处理器都会执行。

```html
<p>A paragraph with a <button>button</button>.</p>
<script>
  let para = document.querySelector("p");
  let button = document.querySelector("button");
  para.addEventListener("mousedown", () => {
    console.log("Handler for paragraph.");
  });
  button.addEventListener("mousedown", event => {
    console.log("Handler for button.");
    if (event.button == 2) event.stopPropagation();
  });
</script>
```

大多数事件对象都有`target`属性，指的是事件来源节点。你可以根据该属性防止无意中处理了传播自其他节点的事件。

我们也可以使用`target`属性来创建出特定类型事件的处理网络。例如，如果一个节点中包含了很长的按钮列表，比较方便的处理方式是在外部节点上注册一个点击事件处理器，并根据事件的`target`属性来区分用户按下了哪个按钮，而不是为每个按钮都注册独立的事件处理器。

```html
<button>A</button>
<button>B</button>
<button>C</button>
<script>
  document.body.addEventListener("click", event => {
    if (event.target.nodeName == "BUTTON") {
      console.log("Clicked", event.target.textContent);
    }
  });
</script>
```

## 默认动作

大多数事件都有与其关联的默认动作。若点击链接，就会跳转到链接目标。若点击向下的箭头，浏览器会向下翻页。若右击鼠标，可以得到一个上下文菜单等。

对于大多数类型的事件，JavaScript 事件处理器会在默认行为发生之前调用。若事件处理器不希望执行默认行为（通常是因为已经处理了该事件），会调用`preventDefault`事件对象的方法。

你可以实现你自己的键盘快捷键或交互式菜单。你也可以干扰用户期望的行为。例如，这里实现一个无法跳转的链接。

```html
<a href="https://developer.mozilla.org/">MDN</a>
<script>
  let link = document.querySelector("a");
  link.addEventListener("click", event => {
    console.log("Nope.");
    event.preventDefault();
  });
</script>
```

除非你有非常充足的理由，否则不要这样做。当预期的行为被打破时，使用你的页面的人会感到不快。

在有些浏览器中，你完全无法拦截某些事件。比如在 Chrome 中，关闭键盘快捷键（`CTRL-W`或`COMMAND-W`）无法由 JavaScript 处理。

## 按键事件

当按下键盘上的按键时，浏览器会触发`"keydown"`事件。当松开按键时，会触发`"keyup"`事件。

```html
<p>This page turns violet when you hold the V key.</p>
<script>
  window.addEventListener("keydown", event => {
    if (event.key == "v") {
      document.body.style.background = "violet";
    }
  });
  window.addEventListener("keyup", event => {
    if (event.key == "v") {
      document.body.style.background = "";
    }
  });
</script>
```

尽管从`keydown`这个事件名上看应该是物理按键按下时触发，但当持续按下某个按键时，会循环触发该事件。有时，你想谨慎对待它。例如，如果您在按下某个按键时向 DOM 添加按钮，并且在释放按键时再次将其删除，则可能会在按住某个按键的时间过长时，意外添加数百个按钮。

该示例查看了事件对象的`key`属性，来查看事件关于哪个键。 该属性包含一个字符串，对于大多数键，它对应于按下该键时将键入的内容。 对于像`Enter`这样的特殊键，它包含一个用于命名键的字符串（在本例中为`"Enter"`）。 如果你按住一个键的同时按住`Shift`键，这也可能影响键的名称 - `"v"`变为`"V"`，`"1"`可能变成`"!"`，这是按下`Shift-1`键 在键盘上产生的东西。

诸如`shift`、`ctrl`、`alt`和`meta`（Mac 上的`command`）之类的修饰按键会像普通按键一样产生事件。但在查找组合键时，你也可以查看键盘和鼠标事件的`shiftKey`、`ctrlKey`、`altKey`和`metaKey`属性来判断这些键是否被按下。

```html
<p>Press Ctrl-Space to continue.</p>
<script>
  window.addEventListener("keydown", event => {
    if (event.key == " " && event.ctrlKey) {
      console.log("Continuing!");
    }
  });
</script>
```

按键事件发生的 DOM 节点取决于按下按键时具有焦点的元素。 大多数节点不能拥有焦点，除非你给他们一个`tabindex`属性，但像链接，按钮和表单字段可以。 我们将在第 18 章中回顾表单字段。 当没有特别的焦点时，`document.body`充当按键事件的目标节点。

当用户键入文本时，使用按键事件来确定正在键入的内容是有问题的。 某些平台，尤其是 Android 手机上的虚拟键盘，不会触发按键事件。 但即使你有一个老式键盘，某些类型的文本输入也不能直接匹配按键，例如其脚本不适合键盘的人所使用的 IME（“输入法编辑器”）软件 ，其中组合多个热键来创建字符。

要注意什么时候输入了内容，每当用户更改其内容时，可以键入的元素（例如`<input>`和`<textarea>`标签）触发`"input"`事件。为了获得输入的实际内容，最好直接从焦点字段中读取它。 第 18 章将展示如何实现。

## 指针事件

目前有两种广泛使用的方式，用于指向屏幕上的东西：鼠标（包括类似鼠标的设备，如触摸板和轨迹球）和触摸屏。 它们产生不同类型的事件。

## 鼠标点击

点击鼠标按键会触发一系列事件。`"mousedown"`事件和`"mouseup"`事件类似于`"keydown"`和`"keyup"`事件，当鼠标按钮按下或释放时触发。当事件发生时，由鼠标指针下方的 DOM 节点触发事件。

在`mouseup`事件后，包含鼠标按下与释放的特定节点会触发`"click"`事件。例如，如果我在一个段落上按下鼠标，移动到另一个段落上释放鼠标，`"click"`事件会发生在包含这两个段落的元素上。

若两次点击事件触发时机接近，则在第二次点击事件之后，也会触发`"dbclick"`（双击，double-click）事件。

为了获得鼠标事件触发的精确信息，你可以查看事件中的`clientX`和`clientY`属性，包含了事件相对于窗口左上角的坐标（以像素为单位）。或`pageX`和`pageY`，它们相对于整个文档的左上角（当窗口被滚动时可能不同）。

下面的代码实现了简单的绘图程序。每次点击文档时，会在鼠标指针下添加一个点。还有一个稍微优化的绘图程序，请参见第 19 章。

```html
<style>
  body {
    height: 200px;
    background: beige;
  }
  .dot {
    height: 8px; width: 8px;
    border-radius: 4px; /* rounds corners */
    background: blue;
    position: absolute;
  }
</style>
<script>
  window.addEventListener("click", event => {
    let dot = document.createElement("div");
    dot.className = "dot";
    dot.style.left = (event.pageX - 4) + "px";
    dot.style.top = (event.pageY - 4) + "px";
    document.body.appendChild(dot);
  });
</script>
```

## 鼠标移动

每次鼠标移动时都会触发`"mousemove"`事件。该事件可用于跟踪鼠标位置。当实现某些形式的鼠标拖拽功能时，该事件非常有用。

举一个例子，下面的程序展示一条栏，并设置一个事件处理器，当向左拖动这个栏时，会使其变窄，若向右拖动则变宽。

```html
<p>Drag the bar to change its width:</p>
<div style="background: orange; width: 60px; height: 20px">
</div>
<script>
  let lastX; // Tracks the last observed mouse X position
  let bar = document.querySelector("div");
  bar.addEventListener("mousedown", event => {
    if (event.button == 0) {
      lastX = event.clientX;
      window.addEventListener("mousemove", moved);
      event.preventDefault(); // Prevent selection
    }
  });


  function moved(event) {
    if (event.buttons == 0) {
      window.removeEventListener("mousemove", moved);
    } else {
      let dist = event.clientX - lastX;
      let newWidth = Math.max(10, bar.offsetWidth + dist);
      bar.style.width = newWidth + "px";
      lastX = event.clientX;
    }
  }
</script>
```


请注意，`mousemove`处理器注册在窗口对象上。即使鼠标在改变窗口尺寸时在栏外侧移动，只要按住按钮，我们仍然想要更新其大小。

释放鼠标按键时，我们必须停止调整栏的大小。 为此，我们可以使用`buttons`属性（注意复数形式），它告诉我们当前按下的按键。 当它为零时，没有按下按键。 当按键被按住时，其值是这些按键的代码总和 - 左键代码为 1，右键为 2，中键为 4。 这样，您可以通过获取`buttons`的剩余值及其代码，来检查是否按下了给定按键。

请注意，这些代码的顺序与`button`使用的顺序不同，中键位于右键之前。 如前所述，一致性并不是浏览器编程接口的强项。

## 触摸事件

我们使用的图形浏览器的风格，是考虑到鼠标界面的情况下而设计的，那个时候触摸屏非常罕见。 为了使网络在早期的触摸屏手机上“工作”，在某种程度上，这些设备的浏览器假装触摸事件是鼠标事件。 如果你点击你的屏幕，你会得到`'mousedown'`，`'mouseup'`和`'click'`事件。

但是这种错觉不是很健壮。 触摸屏与鼠标的工作方式不同：它没有多个按钮，当手指不在屏幕上时不能跟踪手指（来模拟`"mousemove"`），并且允许多个手指同时在屏幕上。

鼠标事件只涵盖了简单情况下的触摸交互 - 如果您为按钮添加`"click"`处理器，触摸用户仍然可以使用它。 但是像上一个示例中的可调整大小的栏在触摸屏上不起作用。

触摸交互触发了特定的事件类型。 当手指开始触摸屏幕时，您会看到`'touchstart'`事件。 当它在触摸中移动时，触发`"touchmove"`事件。 最后，当它停止触摸屏幕时，您会看到`"touchend"`事件。

由于许多触摸屏可以同时检测多个手指，这些事件没有与其关联的一组坐标。 相反，它们的事件对象拥有`touches`属性，它拥有一个类数组对象，每个对象都有自己的`clientX`，`clientY`，`pageX`和`pageY`属性。

你可以这样，在每个触摸手指周围显示红色圆圈。

```html
<style>
  dot { position: absolute; display: block;
        border: 2px solid red; border-radius: 50px;
        height: 100px; width: 100px; }
</style>
<p>Touch this page</p>
<script>
  function update(event) {
    for (let dot; dot = document.querySelector("dot");) {
      dot.remove();
    }
    for (let i = 0; i < event.touches.length; i++) {
      let {pageX, pageY} = event.touches[i];
      let dot = document.createElement("dot");
      dot.style.left = (pageX - 50) + "px";
      dot.style.top = (pageY - 50) + "px";
      document.body.appendChild(dot);
    }
  }
  window.addEventListener("touchstart", update);
  window.addEventListener("touchmove", update);
  window.addEventListener("touchend", update);
</script>
```

您经常希望在触摸事件处理器中调用`preventDefault`，来覆盖浏览器的默认行为（可能包括在滑动时滚动页面），并防止触发鼠标事件，您也可能拥有它的处理器。

## 滚动事件

每当元素滚动时，会触发`scroll`事件。该事件用处极多，比如知道用户当前查看的元素（禁用用户视线以外的动画，或向邪恶的指挥部发送监视报告），或展示一些滚动的迹象（通过高亮表格的部分内容，或显示页码）。

以下示例在文档上方绘制一个进度条，并在您向下滚动时更新它来填充：

```html
<style>
  #progress {
    border-bottom: 2px solid blue;
    width: 0;
    position: fixed;
    top: 0; left: 0;
  }
</style>
<div id="progress"></div>
<script>
  // Create some content
  document.body.appendChild(document.createTextNode(
    "supercalifragilisticexpialidocious ".repeat(1000)));

  let bar = document.querySelector("#progress");
  window.addEventListener("scroll", () => {
    let max = document.body.scrollHeight - innerHeight;
    bar.style.width = `${(pageYOffset / max) * 100}%`;
  });
</script>
```

将元素的`position`属性指定为`fixed`时，其行为和`absolute`很像，但可以防止在文档滚动时期跟着文档一起滚动。其效果是让我们的进度条呆在最顶上。 改变其宽度来指示当前进度。 在设置宽度时，我们使用`%`而不是`px`作为单位，使元素的大小相对于页面宽度。

`innerHeight`全局绑定是窗口高度，我们必须要减去滚动条的高度。你点击文档底部的时候是无法继续滚动的。对于窗口高度来说，也存在`innerWidth`。使用`pageYOffset`（当前滚动位置）除以最大滚动位置，并乘以 100，就可以得到进度条长度。

调用滚动事件的`preventDefault`无法阻止滚动。实际上，事件处理器是在进行滚动之后才触发的。

## 焦点事件

当元素获得焦点时，浏览器会触发其上的`focus`事件。当失去焦点时，元素会获得`blur`事件。

与前文讨论的事件不同，这两个事件不会传播。子元素获得或失去焦点时，不会激活父元素的处理器。

下面的示例中，文本域在拥有焦点时会显示帮助文本。

```html
<p>Name: <input type="text" data-help="Your full name"></p>
<p>Age: <input type="text" data-help="Your age in years"></p>
<p id="help"></p>

<script>
  let help = document.querySelector("#help");
  let fields = document.querySelectorAll("input");
  for (let field of Array.from(fields)) {
    field.addEventListener("focus", event => {
      let text = event.target.getAttribute("data-help");
      help.textContent = text;
    });
    field.addEventListener("blur", event => {
      help.textContent = "";
    });
  }
</script>
```

当用户从浏览器标签或窗口移开时，窗口对象会收到`focus`事件，当移动到标签或窗口上时，则收到`blur`事件。

## 加载事件

当界面结束装载时，会触发窗口对象和文档`body`对象的`"load"`事件。该事件通常用于在当整个文档构建完成时，进行初始化。请记住`<script>标`签的内容是一遇到就执行的。这可能太早了，比如有时脚本需要处理在`<script>`标签后出现的内容。

诸如`image`或`script`这类会装载外部文件的标签都有`load`事件，指示其引用文件装载完毕。类似于焦点事件，装载事件是不会传播的。

当页面关闭或跳转（比如跳转到一个链接）时，会触发`beforeunload`事件。该事件用于防止用户突然关闭文档而丢失工作结果。你无法使用`preventDefault`方法阻止页面卸载。它通过从处理器返回非空值来完成。当你这样做时，浏览器会通过显示一个对话框，询问用户是否关闭页面的对话框中。该机制确保用户可以离开，即使在那些想要留住用户，强制用户看广告的恶意页面上，也是这样。

## 事件和事件循环

在事件循环的上下文中，如第 11 章中所述，浏览器事件处理器的行为，类似于其他异步通知。 它们是在事件发生时调度的，但在它们有机会运行之前，必须等待其他正在运行的脚本完成。

仅当没有别的事情正在运行时，才能处理事件，这个事实意味着，如果事件循环与其他工作捆绑在一起，任何页面交互（通过事件发生）都将延迟，直到有时间处理它为止。 因此，如果您安排了太多工作，无论是长时间运行的事件处理器还是大量短时间运行的工作，该页面都会变得缓慢且麻烦。

如果您想在背后做一些耗时的事情而不会冻结页面，浏览器会提供一些名为 Web Worker 的东西。 Web Worker 是一个 JavaScript 过程，与主脚本一起在自己的时间线上运行。

想象一下，计算一个数字的平方运算是一个重量级的，长期运行的计算，我们希望在一个单独的线程中执行。 我们可以编写一个名为`code/squareworker.js`的文件，通过计算平方并发回消息来响应消息：

```js
addEventListener("message", event => {
  postMessage(event.data * event.data);
});
```

为了避免多线程触及相同数据的问题，Web Worker 不会将其全局作用域或任何其他数据与主脚本的环境共享。 相反，你必须通过来回发送消息与他们沟通。

此代码会生成一个运行该脚本的 Web Worker，向其发送几条消息并输出响应。

```js
let squareWorker = new Worker("code/squareworker.js");
squareWorker.addEventListener("message", event => {
  console.log("The worker responded:", event.data);
});
squareWorker.postMessage(10);
squareWorker.postMessage(24);
```

函数`postMessage`会发送一条消息，触发接收方的`message`事件。创建工作单元的脚本通过`Worker`对象收发消息，而`worker`则直接向其全局作用域发送消息，或监听其消息。只有可以表示为 JSON 的值可以作为消息发送 - 另一方将接收它们的副本，而不是值本身。

## 定时器

我们在第 11 章中看到了`setTimeout`函数。 它会在给定的毫秒数之后，调度另一个函数在稍后调用。

有时读者需要取消调度的函数。可以存储`setTimeout`的返回值，并将作为参数调用`clearTimeout`。

```html
let bombTimer = setTimeout(() => {
  console.log("BOOM!");
}, 500);

if (Math.random() < 0.5) { // 50% chance
  console.log("Defused.");
  clearTimeout(bombTimer);
}
```

函数`cancelAnimationFrame`作用与`clearTimeout`相同，使用`requestAnimationFrame`的返回值调用该函数，可以取消帧（假定函数还没有被调用）。

还有`setInterval`和`clearInterval`这种相似的函数，用于设置计时器，每隔一定毫秒数重复执行一次。

```html
let ticks = 0;
let clock = setInterval(() => {
  console.log("tick", ticks++);
  if (ticks == 10) {
    clearInterval(clock);
    console.log("stop.");
  }
}, 200);
```

## 降频

某些类型的事件可能会连续、迅速触发多次（例如`mousemove`和`scroll`事件）。处理这类事件时，你必须小心谨慎，防止处理任务耗时过长，否则处理器会占据过多事件，导致用户与文档交互变得非常慢。

若你需要在这类处理器中编写一些重要任务，可以使用`setTimeout`来确保不会频繁进行这些任务。我们通常称之为“事件降频（Debounce）”。有许多方法可以完成该任务。

在第一个示例中，当用户输入某些字符时，我们想要有所反应，但我们不想在每个按键事件中立即处理该任务。当用户输入过快时，我们希望暂停一下然后进行处理。我们不是立即在事件处理器中执行动作，而是设置一个定时器。我们也会清除上一次的定时器（如果有），因此当两个事件触发间隔过短（比定时器延时短），就会取消上一次事件设置的定时器。

```html
<textarea>Type something here...</textarea>
<script>
  let textarea = document.querySelector("textarea");
  let timeout;
  textarea.addEventListener("input", () => {
    clearTimeout(timeout);
    timeout = setTimeout(() => console.log("Typed!"), 500);
  });
</script>
```

将`undefined`传递给`clearTimeout`或在一个已结束的定时器上调用`clearTimeout`是没有效果的。因此，我们不需要关心何时调用该方法，只需要每个事件中都这样做即可。

如果我们想要保证每次响应之间至少间隔一段时间，但不希望每次事件发生时都重置定时器，而是在一连串事件连续发生时能够定时触发响应，那么我们可以使用一个略有区别的方法来解决问题。例如，我们想要响应`"mousemove"`事件来显示当前鼠标坐标，但频率只有 250ms。

```html
<script>
  let scheduled = null;
  window.addEventListener("mousemove", event => {
    if (!scheduled) {
      setTimeout(() => {
        document.body.textContent =
          `Mouse at ${scheduled.pageX}, ${scheduled.pageY}`;
        scheduled = null;
      }, 250);
    }
    scheduled = event;
  });
</script>
```

## 本章小结

事件处理器可以检测并响应发生在我们的 Web 页面上的事件。`addEventListener`方法用于注册处理器。

每个事件都有标识事件的类型（`keydown`、`focus`等）。大多数方法都会在特定 DOM 元素上调用，接着向其父节点传播，允许每个父元素的处理器都能处理这些事件。

JavaScript 调用事件处理器时，会传递一个包含事件额外信息的事件对象。该对象也有方法支持停止进一步传播（`stopPropagation`），也支持阻止浏览器执行事件的默认处理器（`preventDefault`）。

按下键盘按键时会触发`keydown`和`keyup`事件。按下鼠标按钮时，会触发`mousedown`、`mouseup`和`click`事件。移动鼠标会触发`mousemove`事件。触摸屏交互会导致`"touchstart"`，`"touchmove"`和`"touchend"`事件。

我们可以通过`scroll`事件监测滚动行为，可以通过`focus`和`blur`事件监控焦点改变。当文档完成加载后，会触发窗口的`load`事件。

## 习题

### 气球

编写一个显示气球的页面（使用气球 emoji，`\ud83c\udf88`）。 当你按下上箭头时，它应该变大（膨胀）10%，而当你按下下箭头时，它应该缩小（放气）10%。

您可以通过在其父元素上设置`font-size` CSS 属性（`style.fontSize`）来控制文本大小（emoji 是文本）。 请记住在该值中包含一个单位，例如像素（`10px`）。

箭头键的键名是`"ArrowUp"`和`"ArrowDown"`。确保按键只更改气球，而不滚动页面。

实现了之后，添加一个功能，如果你将气球吹过一定的尺寸，它就会爆炸。 在这种情况下，爆炸意味着将其替换为“爆炸 emoji，`\ud83d\udca5`”，并且移除事件处理器（以便您不能使爆炸变大变小）。

```html
<p>&#x1f4a5;</p>
<script>
  // Your code here
</script>
```

### 鼠标轨迹

在 JavaScript 早期，有许多主页都会在页面上使用大量的动画，人们想出了许多该语言的创造性用法。

其中一种是“鼠标踪迹”，也就是一系列的元素，随着你在页面上移动鼠标，它会跟着你的鼠标指针。

在本习题中实现鼠标轨迹的功能。使用绝对定位、固定尺寸的`<div>`元素，背景为黑色（请参考鼠标点击一节中的示例）。创建一系列此类元素，当鼠标移动时，伴随鼠标指针显示它们。

有许多方案可以实现我们所需的功能。你可以根据你的需要实现简单的或复杂的方法。简单的解决方案是保存固定鼠标的轨迹元素并循环使用它们，每次`mousemove`事件触发时将下一个元素移动到鼠标当前位置。

```html
<style>
  .trail { /* className for the trail elements */
    position: absolute;
    height: 6px; width: 6px;
    border-radius: 3px;
    background: teal;
  }
  body {
    height: 300px;
  }
</style>

<script>
  // Your code here.
</script>
```

### 选项卡

选项卡面板广泛用于用户界面。它支持用户通过选择元素上方的很多突出的选项卡来选择一个面板。

本习题中，你必须实现一个简单的选项卡界面。编写`asTabs`函数，接受一个 DOM 节点并创建选项卡界面来展现该节点的子元素。该函数应该在顶层节点中插入大量`<button>`元素，与每个子元素一一对应，按钮文本从子节点的`data-tabname`中获取。除了显示一个初始子节点，其他子节点都应该隐藏（将`display`样式设置成`none`），并通过点击按钮来选择当前显示的节点。

当它生效时将其扩展，为当前选中的选项卡，将按钮的样式设为不同的，以便明确选择了哪个选项卡。

```html
<tab-panel>
  <div data-tabname="one">Tab one</div>
  <div data-tabname="two">Tab two</div>
  <div data-tabname="three">Tab three</div>
</tab-panel>
<script>
  function asTabs(node) {
    // Your code here.
  }
  asTabs(document.querySelector("tab-panel"));
</script>
```
