1
0
mirror of https://github.com/apachecn/eloquent-js-3e-zh.git synced 2025-05-23 20:02:20 +00:00
wizardforcel e71c868bfd 15
2018-05-12 16:00:56 +08:00

589 lines
30 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

## 十五、处理事件
> 原文:[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>
```