十五、處理事件
譯者:飛龍
自豪地采用谷歌翻譯
你對你的大腦擁有控制權,而不是外部事件。認識到這一點,你就找到了力量。
馬可·奧勒留,《沉思錄》
https://raw.githubusercontent.com/wizardforcel/eloquent-js-3e-zh/master/img/15-0.jpg
有些程序處理用戶的直接輸入,比如鼠標和鍵盤動作。這種輸入方式不是組織整齊的數據結構 - 它是一次一個地,實時地出現的,并且期望程序在發生時作出響應。
事件處理器
想象一下,有一個接口,若想知道鍵盤上是否有一個鍵是否被按下,唯一的方法是讀取那個按鍵的當前狀態。為了能夠響應按鍵動作,你需要不斷讀取鍵盤狀態,以在按鍵被釋放之前捕捉到按下狀態。這種方法在執行時間密集計算時非常危險,因為你可能錯過按鍵事件。
一些原始機器可以像那樣處理輸入。有一種更進一步的方法,硬件或操作系統發現按鍵時間并將其放入隊列中。程序可以周期性地檢查隊列,等待新事件并在發現事件時進行響應。
當然,程序必須記得監視隊列,并經常做這種事,因為任何時候,按鍵被按下和程序發現事件之間都會使得軟件反應遲鈍。該方法被稱為輪詢。大多數程序員更希望避免這種方法。
一個更好的機制是,系統在發生事件時主動通知我們的代碼。瀏覽器實現了這種特性,支持我們將函數注冊為特定事件的處理器。
<p>Click this document to activate the handler.</p>
<script>
window.addEventListener("click", () => {
console.log("You knocked?");
});
</script>
window
綁定指向瀏覽器提供的內置對象。 它代表包含文檔的瀏覽器窗口。 調用它的addEventListener
方法注冊第二個參數,以便在第一個參數描述的事件發生時調用它。
事件與 DOM 節點
每個瀏覽器事件處理器被注冊在上下文中。在為整個窗口注冊處理器之前,我們在window
對象上調用了addEventListener
。 這種方法也可以在 DOM 元素和一些其他類型的對象上找到。 僅當事件發生在其注冊對象的上下文中時,才調用事件監聽器。
<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
的參數調用。
<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屬性。
<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
,調度段落上的事件處理器執行。當點擊鼠標其他鍵時,兩個處理器都會執行。
<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
屬性來區分用戶按下了哪個按鈕,而不是為每個按鈕都注冊獨立的事件處理器。
<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
事件對象的方法。
你可以實現你自己的鍵盤快捷鍵或交互式菜單。你也可以干擾用戶期望的行為。例如,這里實現一個無法跳轉的鏈接。
<a >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"
事件。
<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
屬性來判斷這些鍵是否被按下。
<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 章。
<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"
事件。該事件可用于跟蹤鼠標位置。當實現某些形式的鼠標拖拽功能時,該事件非常有用。
舉一個例子,下面的程序展示一條欄,并設置一個事件處理器,當向左拖動這個欄時,會使其變窄,若向右拖動則變寬。
<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
屬性。
你可以這樣,在每個觸摸手指周圍顯示紅色圓圈。
<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
事件。該事件用處極多,比如知道用戶當前查看的元素(禁用用戶視線以外的動畫,或向邪惡的指揮部發送監視報告),或展示一些滾動的跡象(通過高亮表格的部分內容,或顯示頁碼)。
以下示例在文檔上方繪制一個進度條,并在您向下滾動時更新它來填充:
<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
事件。
與前文討論的事件不同,這兩個事件不會傳播。子元素獲得或失去焦點時,不會激活父元素的處理器。
下面的示例中,文本域在擁有焦點時會顯示幫助文本。
<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
的文件,通過計算平方并發回消息來響應消息:
addEventListener("message", event => {
postMessage(event.data * event.data);
});
為了避免多線程觸及相同數據的問題,Web Worker 不會將其全局作用域或任何其他數據與主腳本的環境共享。 相反,你必須通過來回發送消息與他們溝通。
此代碼會生成一個運行該腳本的 Web Worker,向其發送幾條消息并輸出響應。
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
。
let bombTimer = setTimeout(() => {
console.log("BOOM!");
}, 500);
if (Math.random() < 0.5) { // 50% chance
console.log("Defused.");
clearTimeout(bombTimer);
}
函數cancelAnimationFrame
作用與clearTimeout
相同,使用requestAnimationFrame
的返回值調用該函數,可以取消幀(假定函數還沒有被調用)。
還有setInterval
和clearInterval
這種相似的函數,用于設置計時器,每隔一定毫秒數重復執行一次。
let ticks = 0;
let clock = setInterval(() => {
console.log("tick", ticks++);
if (ticks == 10) {
clearInterval(clock);
console.log("stop.");
}
}, 200);
降頻
某些類型的事件可能會連續、迅速觸發多次(例如mousemove
和scroll
事件)。處理這類事件時,你必須小心謹慎,防止處理任務耗時過長,否則處理器會占據過多事件,導致用戶與文檔交互變得非常慢。
若你需要在這類處理器中編寫一些重要任務,可以使用setTimeout
來確保不會頻繁進行這些任務。我們通常稱之為“事件降頻(Debounce)”。有許多方法可以完成該任務。
在第一個示例中,當用戶輸入某些字符時,我們想要有所反應,但我們不想在每個按鍵事件中立即處理該任務。當用戶輸入過快時,我們希望暫停一下然后進行處理。我們不是立即在事件處理器中執行動作,而是設置一個定時器。我們也會清除上一次的定時器(如果有),因此當兩個事件觸發間隔過短(比定時器延時短),就會取消上一次事件設置的定時器。
<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。
<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
”,并且移除事件處理器(以便您不能使爆炸變大變小)。
<p>💥</p>
<script>
// Your code here
</script>
鼠標軌跡
在 JavaScript 早期,有許多主頁都會在頁面上使用大量的動畫,人們想出了許多該語言的創造性用法。
其中一種是“鼠標蹤跡”,也就是一系列的元素,隨著你在頁面上移動鼠標,它會跟著你的鼠標指針。
在本習題中實現鼠標軌跡的功能。使用絕對定位、固定尺寸的<div>
元素,背景為黑色(請參考鼠標點擊一節中的示例)。創建一系列此類元素,當鼠標移動時,伴隨鼠標指針顯示它們。
有許多方案可以實現我們所需的功能。你可以根據你的需要實現簡單的或復雜的方法。簡單的解決方案是保存固定鼠標的軌跡元素并循環使用它們,每次mousemove
事件觸發時將下一個元素移動到鼠標當前位置。
<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
),并通過點擊按鈕來選擇當前顯示的節點。
當它生效時將其擴展,為當前選中的選項卡,將按鈕的樣式設為不同的,以便明確選擇了哪個選項卡。
<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>