## 十五、處理事件
> 原文:[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/)
> 你對你的大腦擁有控制權,而不是外部事件。認識到這一點,你就找到了力量。
>
> 馬可·奧勒留,《沉思錄》

有些程序處理用戶的直接輸入,比如鼠標和鍵盤動作。這種輸入方式不是組織整齊的數據結構 - 它是一次一個地,實時地出現的,并且期望程序在發生時作出響應。
## 事件處理器
想象一下,有一個接口,若想知道鍵盤上是否有一個鍵是否被按下,唯一的方法是讀取那個按鍵的當前狀態。為了能夠響應按鍵動作,你需要不斷讀取鍵盤狀態,以在按鍵被釋放之前捕捉到按下狀態。這種方法在執行時間密集計算時非常危險,因為你可能錯過按鍵事件。
一些原始機器可以像那樣處理輸入。有一種更進一步的方法,硬件或操作系統發現按鍵時間并將其放入隊列中。程序可以周期性地檢查隊列,等待新事件并在發現事件時進行響應。
當然,程序必須記得監視隊列,并經常做這種事,因為任何時候,按鍵被按下和程序發現事件之間都會使得軟件反應遲鈍。該方法被稱為輪詢。大多數程序員更希望避免這種方法。
一個更好的機制是,系統在發生事件時主動通知我們的代碼。瀏覽器實現了這種特性,支持我們將函數注冊為特定事件的處理器。
```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>💥</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>
```