十五、處理事件
譯者:飛龍
協(xié)議:CC BY-NC-SA 4.0
自豪地采用谷歌翻譯
你對(duì)你的大腦擁有控制權(quán)崩侠,而不是外部事件傀缩。認(rèn)識(shí)到這一點(diǎn)那先,你就找到了力量。
馬可·奧勒留赡艰,《沉思錄》
https://raw.githubusercontent.com/wizardforcel/eloquent-js-3e-zh/master/img/15-0.jpg
有些程序處理用戶的直接輸入售淡,比如鼠標(biāo)和鍵盤動(dòng)作。這種輸入方式不是組織整齊的數(shù)據(jù)結(jié)構(gòu) - 它是一次一個(gè)地慷垮,實(shí)時(shí)地出現(xiàn)的揖闸,并且期望程序在發(fā)生時(shí)作出響應(yīng)。
事件處理器
想象一下料身,有一個(gè)接口汤纸,若想知道鍵盤上是否有一個(gè)鍵是否被按下,唯一的方法是讀取那個(gè)按鍵的當(dāng)前狀態(tài)芹血。為了能夠響應(yīng)按鍵動(dòng)作贮泞,你需要不斷讀取鍵盤狀態(tài),以在按鍵被釋放之前捕捉到按下狀態(tài)幔烛。這種方法在執(zhí)行時(shí)間密集計(jì)算時(shí)非常危險(xiǎn)啃擦,因?yàn)槟憧赡苠e(cuò)過按鍵事件。
一些原始機(jī)器可以像那樣處理輸入饿悬。有一種更進(jìn)一步的方法令蛉,硬件或操作系統(tǒng)發(fā)現(xiàn)按鍵時(shí)間并將其放入隊(duì)列中。程序可以周期性地檢查隊(duì)列狡恬,等待新事件并在發(fā)現(xiàn)事件時(shí)進(jìn)行響應(yīng)珠叔。
當(dāng)然,程序必須記得監(jiān)視隊(duì)列傲宜,并經(jīng)常做這種事运杭,因?yàn)槿魏螘r(shí)候,按鍵被按下和程序發(fā)現(xiàn)事件之間都會(huì)使得軟件反應(yīng)遲鈍函卒。該方法被稱為輪詢辆憔。大多數(shù)程序員更希望避免這種方法。
一個(gè)更好的機(jī)制是报嵌,系統(tǒng)在發(fā)生事件時(shí)主動(dòng)通知我們的代碼虱咧。瀏覽器實(shí)現(xiàn)了這種特性,支持我們將函數(shù)注冊為特定事件的處理器锚国。
<p>Click this document to activate the handler.</p>
<script>
window.addEventListener("click", () => {
console.log("You knocked?");
});
</script>
window
綁定指向?yàn)g覽器提供的內(nèi)置對(duì)象腕巡。 它代表包含文檔的瀏覽器窗口。 調(diào)用它的addEventListener
方法注冊第二個(gè)參數(shù)血筑,以便在第一個(gè)參數(shù)描述的事件發(fā)生時(shí)調(diào)用它绘沉。
事件與 DOM 節(jié)點(diǎn)
每個(gè)瀏覽器事件處理器被注冊在上下文中煎楣。在為整個(gè)窗口注冊處理器之前,我們在window
對(duì)象上調(diào)用了addEventListener
车伞。 這種方法也可以在 DOM 元素和一些其他類型的對(duì)象上找到择懂。 僅當(dāng)事件發(fā)生在其注冊對(duì)象的上下文中時(shí),才調(diào)用事件監(jiān)聽器另玖。
<button>Click me</button>
<p>No handler here.</p>
<script>
let button = document.querySelector("button");
button.addEventListener("click", () => {
console.log("Button clicked.");
});
</script>
示例代碼中將處理器附加到按鈕節(jié)點(diǎn)上困曙。因此,點(diǎn)擊按鈕時(shí)會(huì)觸發(fā)并執(zhí)行處理器谦去,而點(diǎn)擊文檔的其他部分則沒有反應(yīng)慷丽。
向節(jié)點(diǎn)提供onclick
屬性也有類似效果。這適用于大多數(shù)類型的事件 - 您可以為屬性附加處理器鳄哭,屬性名稱為前面帶有on
的事件名稱要糊。
但是一個(gè)節(jié)點(diǎn)只能有一個(gè)onclick
屬性,所以你只能用這種方式為每個(gè)節(jié)點(diǎn)注冊一個(gè)處理器窃诉。 addEventListener
方法允許您添加任意數(shù)量的處理器杨耙,因此即使元素上已經(jīng)存在另一個(gè)處理器,添加處理器也是安全的飘痛。
removeEventListener
方法將刪除一個(gè)處理器珊膜,使用類似于addEventListener
的參數(shù)調(diào)用。
<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
的函數(shù)必須是賦予addEventListener
的完全相同的函數(shù)值宣脉。 因此车柠,要注銷一個(gè)處理其,您需要為該函數(shù)提供一個(gè)名稱(在本例中為once
)塑猖,以便能夠?qū)⑾嗤暮瘮?shù)值傳遞給這兩個(gè)方法竹祷。
事件對(duì)象
雖然目前為止我們忽略了它,事件處理器函數(shù)作為對(duì)象傳遞:事件(Event)對(duì)象羊苟。這個(gè)對(duì)象持有事件的額外信息塑陵。例如,如果我們想知道哪個(gè)鼠標(biāo)按鍵被按下蜡励,我們可以查看事件對(duì)象的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>
存儲(chǔ)在各種類型事件對(duì)象中的信息是有差別的。隨后本章將會(huì)討論許多類型的事件凉倚。對(duì)象的type
屬性一般持有一個(gè)字符串兼都,表示事件(例如"click"
和"mousedown"
)。
傳播
對(duì)于大多數(shù)事件類型稽寒,在具有子節(jié)點(diǎn)的節(jié)點(diǎn)上注冊的處理器扮碧,也將接收發(fā)生在子節(jié)點(diǎn)中的事件。若點(diǎn)擊一個(gè)段落中的按鈕,段落的事件處理器也會(huì)收到點(diǎn)擊事件慎王。
但若段落和按鈕都有事件處理器蚓土,則先執(zhí)行最特殊的事件處理器(按鈕的事件處理器)。也就是說事件向外傳播柬祠,從觸發(fā)事件的節(jié)點(diǎn)到其父節(jié)點(diǎn)北戏,最后直到文檔根節(jié)點(diǎn)。最后漫蛔,當(dāng)某個(gè)特定節(jié)點(diǎn)上注冊的所有事件處理器按其順序全部執(zhí)行完畢后,窗口對(duì)象的事件處理器才有機(jī)會(huì)響應(yīng)事件旧蛾。
事件處理器任何時(shí)候都可以調(diào)用事件對(duì)象的stopPropagation
方法莽龟,阻止事件進(jìn)一步傳播。該方法有時(shí)很實(shí)用锨天,例如毯盈,你將一個(gè)按鈕放在另一個(gè)可點(diǎn)擊元素中,但你不希望點(diǎn)擊該按鈕會(huì)激活外部元素的點(diǎn)擊行為病袄。
下面的示例代碼將mousedown
處理器注冊到按鈕和其外部的段落節(jié)點(diǎn)上搂赋。在按鈕上點(diǎn)擊鼠標(biāo)右鍵,按鈕的處理器會(huì)調(diào)用stopPropagation
益缠,調(diào)度段落上的事件處理器執(zhí)行脑奠。當(dāng)點(diǎn)擊鼠標(biāo)其他鍵時(shí),兩個(gè)處理器都會(huì)執(zhí)行幅慌。
<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>
大多數(shù)事件對(duì)象都有target
屬性宋欺,指的是事件來源節(jié)點(diǎn)。你可以根據(jù)該屬性防止無意中處理了傳播自其他節(jié)點(diǎn)的事件胰伍。
我們也可以使用target
屬性來創(chuàng)建出特定類型事件的處理網(wǎng)絡(luò)齿诞。例如,如果一個(gè)節(jié)點(diǎn)中包含了很長的按鈕列表骂租,比較方便的處理方式是在外部節(jié)點(diǎn)上注冊一個(gè)點(diǎn)擊事件處理器祷杈,并根據(jù)事件的target
屬性來區(qū)分用戶按下了哪個(gè)按鈕,而不是為每個(gè)按鈕都注冊獨(dú)立的事件處理器渗饮。
<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>
默認(rèn)動(dòng)作
大多數(shù)事件都有與其關(guān)聯(lián)的默認(rèn)動(dòng)作但汞。若點(diǎn)擊鏈接,就會(huì)跳轉(zhuǎn)到鏈接目標(biāo)抽米。若點(diǎn)擊向下的箭頭特占,瀏覽器會(huì)向下翻頁。若右擊鼠標(biāo)云茸,可以得到一個(gè)上下文菜單等是目。
對(duì)于大多數(shù)類型的事件,JavaScript 事件處理器會(huì)在默認(rèn)行為發(fā)生之前調(diào)用标捺。若事件處理器不希望執(zhí)行默認(rèn)行為(通常是因?yàn)橐呀?jīng)處理了該事件)懊纳,會(huì)調(diào)用preventDefault
事件對(duì)象的方法揉抵。
你可以實(shí)現(xiàn)你自己的鍵盤快捷鍵或交互式菜單。你也可以干擾用戶期望的行為嗤疯。例如冤今,這里實(shí)現(xiàn)一個(gè)無法跳轉(zhuǎn)的鏈接。
<a >MDN</a>
<script>
let link = document.querySelector("a");
link.addEventListener("click", event => {
console.log("Nope.");
event.preventDefault();
});
</script>
除非你有非常充足的理由茂缚,否則不要這樣做戏罢。當(dāng)預(yù)期的行為被打破時(shí),使用你的頁面的人會(huì)感到不快脚囊。
在有些瀏覽器中龟糕,你完全無法攔截某些事件。比如在 Chrome 中悔耘,關(guān)閉鍵盤快捷鍵(CTRL-W
或COMMAND-W
)無法由 JavaScript 處理讲岁。
按鍵事件
當(dāng)按下鍵盤上的按鍵時(shí),瀏覽器會(huì)觸發(fā)"keydown"
事件衬以。當(dāng)松開按鍵時(shí)缓艳,會(huì)觸發(fā)"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
這個(gè)事件名上看應(yīng)該是物理按鍵按下時(shí)觸發(fā)看峻,但當(dāng)持續(xù)按下某個(gè)按鍵時(shí)阶淘,會(huì)循環(huán)觸發(fā)該事件。有時(shí)备籽,你想謹(jǐn)慎對(duì)待它舶治。例如,如果您在按下某個(gè)按鍵時(shí)向 DOM 添加按鈕车猬,并且在釋放按鍵時(shí)再次將其刪除霉猛,則可能會(huì)在按住某個(gè)按鍵的時(shí)間過長時(shí),意外添加數(shù)百個(gè)按鈕珠闰。
該示例查看了事件對(duì)象的key
屬性惜浅,來查看事件關(guān)于哪個(gè)鍵。 該屬性包含一個(gè)字符串伏嗜,對(duì)于大多數(shù)鍵坛悉,它對(duì)應(yīng)于按下該鍵時(shí)將鍵入的內(nèi)容。 對(duì)于像Enter
這樣的特殊鍵承绸,它包含一個(gè)用于命名鍵的字符串(在本例中為"Enter"
)裸影。 如果你按住一個(gè)鍵的同時(shí)按住Shift
鍵,這也可能影響鍵的名稱 - "v"
變?yōu)?code>"V"军熏,"1"
可能變成"!"
轩猩,這是按下Shift-1
鍵 在鍵盤上產(chǎn)生的東西。
諸如shift
、ctrl
均践、alt
和meta
(Mac 上的command
)之類的修飾按鍵會(huì)像普通按鍵一樣產(chǎn)生事件晤锹。但在查找組合鍵時(shí),你也可以查看鍵盤和鼠標(biāo)事件的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>
按鍵事件發(fā)生的 DOM 節(jié)點(diǎn)取決于按下按鍵時(shí)具有焦點(diǎn)的元素焦影。 大多數(shù)節(jié)點(diǎn)不能擁有焦點(diǎn)车遂,除非你給他們一個(gè)tabindex
屬性,但像鏈接斯辰,按鈕和表單字段可以艰额。 我們將在第 18 章中回顧表單字段。 當(dāng)沒有特別的焦點(diǎn)時(shí)椒涯,document.body
充當(dāng)按鍵事件的目標(biāo)節(jié)點(diǎn)。
當(dāng)用戶鍵入文本時(shí)回梧,使用按鍵事件來確定正在鍵入的內(nèi)容是有問題的废岂。 某些平臺(tái),尤其是 Android 手機(jī)上的虛擬鍵盤狱意,不會(huì)觸發(fā)按鍵事件湖苞。 但即使你有一個(gè)老式鍵盤,某些類型的文本輸入也不能直接匹配按鍵详囤,例如其腳本不適合鍵盤的人所使用的 IME(“輸入法編輯器”)軟件 财骨,其中組合多個(gè)熱鍵來創(chuàng)建字符。
要注意什么時(shí)候輸入了內(nèi)容藏姐,每當(dāng)用戶更改其內(nèi)容時(shí)隆箩,可以鍵入的元素(例如<input>
和<textarea>
標(biāo)簽)觸發(fā)"input"
事件。為了獲得輸入的實(shí)際內(nèi)容羔杨,最好直接從焦點(diǎn)字段中讀取它捌臊。 第 18 章將展示如何實(shí)現(xiàn)。
指針事件
目前有兩種廣泛使用的方式兜材,用于指向屏幕上的東西:鼠標(biāo)(包括類似鼠標(biāo)的設(shè)備理澎,如觸摸板和軌跡球)和觸摸屏。 它們產(chǎn)生不同類型的事件曙寡。
鼠標(biāo)點(diǎn)擊
點(diǎn)擊鼠標(biāo)按鍵會(huì)觸發(fā)一系列事件糠爬。"mousedown"
事件和"mouseup"
事件類似于"keydown"
和"keyup"
事件,當(dāng)鼠標(biāo)按鈕按下或釋放時(shí)觸發(fā)举庶。當(dāng)事件發(fā)生時(shí)皂林,由鼠標(biāo)指針下方的 DOM 節(jié)點(diǎn)觸發(fā)事件。
在mouseup
事件后油昂,包含鼠標(biāo)按下與釋放的特定節(jié)點(diǎn)會(huì)觸發(fā)"click"
事件。例如捅膘,如果我在一個(gè)段落上按下鼠標(biāo),移動(dòng)到另一個(gè)段落上釋放鼠標(biāo)滚粟,"click"
事件會(huì)發(fā)生在包含這兩個(gè)段落的元素上寻仗。
若兩次點(diǎn)擊事件觸發(fā)時(shí)機(jī)接近,則在第二次點(diǎn)擊事件之后凡壤,也會(huì)觸發(fā)"dbclick"
(雙擊署尤,double-click)事件。
為了獲得鼠標(biāo)事件觸發(fā)的精確信息亚侠,你可以查看事件中的clientX
和clientY
屬性曹体,包含了事件相對(duì)于窗口左上角的坐標(biāo)(以像素為單位)∠趵茫或pageX
和pageY
箕别,它們相對(duì)于整個(gè)文檔的左上角(當(dāng)窗口被滾動(dòng)時(shí)可能不同)。
下面的代碼實(shí)現(xiàn)了簡單的繪圖程序滞谢。每次點(diǎn)擊文檔時(shí)串稀,會(huì)在鼠標(biāo)指針下添加一個(gè)點(diǎn)。還有一個(gè)稍微優(yōu)化的繪圖程序狮杨,請(qǐng)參見第 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>
鼠標(biāo)移動(dòng)
每次鼠標(biāo)移動(dòng)時(shí)都會(huì)觸發(fā)"mousemove"
事件。該事件可用于跟蹤鼠標(biāo)位置橄教。當(dāng)實(shí)現(xiàn)某些形式的鼠標(biāo)拖拽功能時(shí)清寇,該事件非常有用。
舉一個(gè)例子护蝶,下面的程序展示一條欄华烟,并設(shè)置一個(gè)事件處理器,當(dāng)向左拖動(dòng)這個(gè)欄時(shí)滓走,會(huì)使其變窄垦江,若向右拖動(dòng)則變寬。
<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>
請(qǐng)注意搅方,mousemove
處理器注冊在窗口對(duì)象上比吭。即使鼠標(biāo)在改變窗口尺寸時(shí)在欄外側(cè)移動(dòng),只要按住按鈕姨涡,我們?nèi)匀幌胍缕浯笮 ?/p>
釋放鼠標(biāo)按鍵時(shí)衩藤,我們必須停止調(diào)整欄的大小。 為此涛漂,我們可以使用buttons
屬性(注意復(fù)數(shù)形式)赏表,它告訴我們當(dāng)前按下的按鍵检诗。 當(dāng)它為零時(shí),沒有按下按鍵瓢剿。 當(dāng)按鍵被按住時(shí)逢慌,其值是這些按鍵的代碼總和 - 左鍵代碼為 1,右鍵為 2间狂,中鍵為 4攻泼。 這樣,您可以通過獲取buttons
的剩余值及其代碼鉴象,來檢查是否按下了給定按鍵忙菠。
請(qǐng)注意,這些代碼的順序與button
使用的順序不同纺弊,中鍵位于右鍵之前牛欢。 如前所述,一致性并不是瀏覽器編程接口的強(qiáng)項(xiàng)淆游。
觸摸事件
我們使用的圖形瀏覽器的風(fēng)格傍睹,是考慮到鼠標(biāo)界面的情況下而設(shè)計(jì)的,那個(gè)時(shí)候觸摸屏非常罕見犹菱。 為了使網(wǎng)絡(luò)在早期的觸摸屏手機(jī)上“工作”焰望,在某種程度上,這些設(shè)備的瀏覽器假裝觸摸事件是鼠標(biāo)事件已亥。 如果你點(diǎn)擊你的屏幕,你會(huì)得到'mousedown'
来屠,'mouseup'
和'click'
事件虑椎。
但是這種錯(cuò)覺不是很健壯。 觸摸屏與鼠標(biāo)的工作方式不同:它沒有多個(gè)按鈕俱笛,當(dāng)手指不在屏幕上時(shí)不能跟蹤手指(來模擬"mousemove"
)捆姜,并且允許多個(gè)手指同時(shí)在屏幕上。
鼠標(biāo)事件只涵蓋了簡單情況下的觸摸交互 - 如果您為按鈕添加"click"
處理器迎膜,觸摸用戶仍然可以使用它泥技。 但是像上一個(gè)示例中的可調(diào)整大小的欄在觸摸屏上不起作用。
觸摸交互觸發(fā)了特定的事件類型磕仅。 當(dāng)手指開始觸摸屏幕時(shí)珊豹,您會(huì)看到'touchstart'
事件。 當(dāng)它在觸摸中移動(dòng)時(shí)榕订,觸發(fā)"touchmove"
事件店茶。 最后,當(dāng)它停止觸摸屏幕時(shí)劫恒,您會(huì)看到"touchend"
事件贩幻。
由于許多觸摸屏可以同時(shí)檢測多個(gè)手指轿腺,這些事件沒有與其關(guān)聯(lián)的一組坐標(biāo)。 相反丛楚,它們的事件對(duì)象擁有touches
屬性族壳,它擁有一個(gè)類數(shù)組對(duì)象,每個(gè)對(duì)象都有自己的clientX
趣些,clientY
仿荆,pageX
和pageY
屬性。
你可以這樣喧务,在每個(gè)觸摸手指周圍顯示紅色圓圈赖歌。
<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>
您經(jīng)常希望在觸摸事件處理器中調(diào)用preventDefault
,來覆蓋瀏覽器的默認(rèn)行為(可能包括在滑動(dòng)時(shí)滾動(dòng)頁面)功茴,并防止觸發(fā)鼠標(biāo)事件庐冯,您也可能擁有它的處理器。
滾動(dòng)事件
每當(dāng)元素滾動(dòng)時(shí)坎穿,會(huì)觸發(fā)scroll
事件展父。該事件用處極多,比如知道用戶當(dāng)前查看的元素(禁用用戶視線以外的動(dòng)畫玲昧,或向邪惡的指揮部發(fā)送監(jiān)視報(bào)告)栖茉,或展示一些滾動(dòng)的跡象(通過高亮表格的部分內(nèi)容,或顯示頁碼)孵延。
以下示例在文檔上方繪制一個(gè)進(jìn)度條吕漂,并在您向下滾動(dòng)時(shí)更新它來填充:
<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
時(shí),其行為和absolute
很像尘应,但可以防止在文檔滾動(dòng)時(shí)期跟著文檔一起滾動(dòng)惶凝。其效果是讓我們的進(jìn)度條呆在最頂上。 改變其寬度來指示當(dāng)前進(jìn)度犬钢。 在設(shè)置寬度時(shí)苍鲜,我們使用%
而不是px
作為單位,使元素的大小相對(duì)于頁面寬度玷犹。
innerHeight
全局綁定是窗口高度混滔,我們必須要減去滾動(dòng)條的高度。你點(diǎn)擊文檔底部的時(shí)候是無法繼續(xù)滾動(dòng)的歹颓。對(duì)于窗口高度來說坯屿,也存在innerWidth
。使用pageYOffset
(當(dāng)前滾動(dòng)位置)除以最大滾動(dòng)位置巍扛,并乘以 100愿伴,就可以得到進(jìn)度條長度。
調(diào)用滾動(dòng)事件的preventDefault
無法阻止?jié)L動(dòng)电湘。實(shí)際上隔节,事件處理器是在進(jìn)行滾動(dòng)之后才觸發(fā)的鹅经。
焦點(diǎn)事件
當(dāng)元素獲得焦點(diǎn)時(shí),瀏覽器會(huì)觸發(fā)其上的focus
事件怎诫。當(dāng)失去焦點(diǎn)時(shí)瘾晃,元素會(huì)獲得blur
事件。
與前文討論的事件不同幻妓,這兩個(gè)事件不會(huì)傳播蹦误。子元素獲得或失去焦點(diǎn)時(shí),不會(huì)激活父元素的處理器肉津。
下面的示例中强胰,文本域在擁有焦點(diǎn)時(shí)會(huì)顯示幫助文本。
<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>
當(dāng)用戶從瀏覽器標(biāo)簽或窗口移開時(shí)妹沙,窗口對(duì)象會(huì)收到focus
事件偶洋,當(dāng)移動(dòng)到標(biāo)簽或窗口上時(shí),則收到blur
事件距糖。
加載事件
當(dāng)界面結(jié)束裝載時(shí)玄窝,會(huì)觸發(fā)窗口對(duì)象和文檔body
對(duì)象的"load"
事件。該事件通常用于在當(dāng)整個(gè)文檔構(gòu)建完成時(shí)悍引,進(jìn)行初始化恩脂。請(qǐng)記住<script>標(biāo)
簽的內(nèi)容是一遇到就執(zhí)行的。這可能太早了趣斤,比如有時(shí)腳本需要處理在<script>
標(biāo)簽后出現(xiàn)的內(nèi)容俩块。
諸如image
或script
這類會(huì)裝載外部文件的標(biāo)簽都有load
事件,指示其引用文件裝載完畢浓领。類似于焦點(diǎn)事件典阵,裝載事件是不會(huì)傳播的。
當(dāng)頁面關(guān)閉或跳轉(zhuǎn)(比如跳轉(zhuǎn)到一個(gè)鏈接)時(shí)镊逝,會(huì)觸發(fā)beforeunload
事件。該事件用于防止用戶突然關(guān)閉文檔而丟失工作結(jié)果嫉鲸。你無法使用preventDefault
方法阻止頁面卸載撑蒜。它通過從處理器返回非空值來完成。當(dāng)你這樣做時(shí)玄渗,瀏覽器會(huì)通過顯示一個(gè)對(duì)話框座菠,詢問用戶是否關(guān)閉頁面的對(duì)話框中。該機(jī)制確保用戶可以離開藤树,即使在那些想要留住用戶浴滴,強(qiáng)制用戶看廣告的惡意頁面上,也是這樣岁钓。
事件和事件循環(huán)
在事件循環(huán)的上下文中升略,如第 11 章中所述微王,瀏覽器事件處理器的行為,類似于其他異步通知品嚣。 它們是在事件發(fā)生時(shí)調(diào)度的炕倘,但在它們有機(jī)會(huì)運(yùn)行之前,必須等待其他正在運(yùn)行的腳本完成翰撑。
僅當(dāng)沒有別的事情正在運(yùn)行時(shí)罩旋,才能處理事件,這個(gè)事實(shí)意味著眶诈,如果事件循環(huán)與其他工作捆綁在一起涨醋,任何頁面交互(通過事件發(fā)生)都將延遲,直到有時(shí)間處理它為止逝撬。 因此浴骂,如果您安排了太多工作,無論是長時(shí)間運(yùn)行的事件處理器還是大量短時(shí)間運(yùn)行的工作球拦,該頁面都會(huì)變得緩慢且麻煩靠闭。
如果您想在背后做一些耗時(shí)的事情而不會(huì)凍結(jié)頁面,瀏覽器會(huì)提供一些名為 Web Worker 的東西坎炼。 Web Worker 是一個(gè) JavaScript 過程愧膀,與主腳本一起在自己的時(shí)間線上運(yùn)行。
想象一下谣光,計(jì)算一個(gè)數(shù)字的平方運(yùn)算是一個(gè)重量級(jí)的檩淋,長期運(yùn)行的計(jì)算,我們希望在一個(gè)單獨(dú)的線程中執(zhí)行萄金。 我們可以編寫一個(gè)名為code/squareworker.js
的文件蟀悦,通過計(jì)算平方并發(fā)回消息來響應(yīng)消息:
addEventListener("message", event => {
postMessage(event.data * event.data);
});
為了避免多線程觸及相同數(shù)據(jù)的問題,Web Worker 不會(huì)將其全局作用域或任何其他數(shù)據(jù)與主腳本的環(huán)境共享氧敢。 相反日戈,你必須通過來回發(fā)送消息與他們溝通。
此代碼會(huì)生成一個(gè)運(yùn)行該腳本的 Web Worker孙乖,向其發(fā)送幾條消息并輸出響應(yīng)浙炼。
let squareWorker = new Worker("code/squareworker.js");
squareWorker.addEventListener("message", event => {
console.log("The worker responded:", event.data);
});
squareWorker.postMessage(10);
squareWorker.postMessage(24);
函數(shù)postMessage
會(huì)發(fā)送一條消息,觸發(fā)接收方的message
事件唯袄。創(chuàng)建工作單元的腳本通過Worker
對(duì)象收發(fā)消息弯屈,而worker
則直接向其全局作用域發(fā)送消息,或監(jiān)聽其消息恋拷。只有可以表示為 JSON 的值可以作為消息發(fā)送 - 另一方將接收它們的副本资厉,而不是值本身。
定時(shí)器
我們在第 11 章中看到了setTimeout
函數(shù)蔬顾。 它會(huì)在給定的毫秒數(shù)之后宴偿,調(diào)度另一個(gè)函數(shù)在稍后調(diào)用湘捎。
有時(shí)讀者需要取消調(diào)度的函數(shù)±椅遥可以存儲(chǔ)setTimeout
的返回值消痛,并將作為參數(shù)調(diào)用clearTimeout
。
let bombTimer = setTimeout(() => {
console.log("BOOM!");
}, 500);
if (Math.random() < 0.5) { // 50% chance
console.log("Defused.");
clearTimeout(bombTimer);
}
函數(shù)cancelAnimationFrame
作用與clearTimeout
相同都哭,使用requestAnimationFrame
的返回值調(diào)用該函數(shù)秩伞,可以取消幀(假定函數(shù)還沒有被調(diào)用)。
還有setInterval
和clearInterval
這種相似的函數(shù)欺矫,用于設(shè)置計(jì)時(shí)器纱新,每隔一定毫秒數(shù)重復(fù)執(zhí)行一次。
let ticks = 0;
let clock = setInterval(() => {
console.log("tick", ticks++);
if (ticks == 10) {
clearInterval(clock);
console.log("stop.");
}
}, 200);
降頻
某些類型的事件可能會(huì)連續(xù)穆趴、迅速觸發(fā)多次(例如mousemove
和scroll
事件)脸爱。處理這類事件時(shí),你必須小心謹(jǐn)慎未妹,防止處理任務(wù)耗時(shí)過長簿废,否則處理器會(huì)占據(jù)過多事件,導(dǎo)致用戶與文檔交互變得非常慢络它。
若你需要在這類處理器中編寫一些重要任務(wù)族檬,可以使用setTimeout
來確保不會(huì)頻繁進(jìn)行這些任務(wù)。我們通常稱之為“事件降頻(Debounce)”化戳。有許多方法可以完成該任務(wù)单料。
在第一個(gè)示例中,當(dāng)用戶輸入某些字符時(shí)点楼,我們想要有所反應(yīng)扫尖,但我們不想在每個(gè)按鍵事件中立即處理該任務(wù)。當(dāng)用戶輸入過快時(shí)掠廓,我們希望暫停一下然后進(jìn)行處理换怖。我們不是立即在事件處理器中執(zhí)行動(dòng)作,而是設(shè)置一個(gè)定時(shí)器蟀瞧。我們也會(huì)清除上一次的定時(shí)器(如果有)沉颂,因此當(dāng)兩個(gè)事件觸發(fā)間隔過短(比定時(shí)器延時(shí)短),就會(huì)取消上一次事件設(shè)置的定時(shí)器黄橘。
<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
或在一個(gè)已結(jié)束的定時(shí)器上調(diào)用clearTimeout
是沒有效果的。因此屈溉,我們不需要關(guān)心何時(shí)調(diào)用該方法塞关,只需要每個(gè)事件中都這樣做即可。
如果我們想要保證每次響應(yīng)之間至少間隔一段時(shí)間子巾,但不希望每次事件發(fā)生時(shí)都重置定時(shí)器帆赢,而是在一連串事件連續(xù)發(fā)生時(shí)能夠定時(shí)觸發(fā)響應(yīng)小压,那么我們可以使用一個(gè)略有區(qū)別的方法來解決問題。例如椰于,我們想要響應(yīng)"mousemove"
事件來顯示當(dāng)前鼠標(biāo)坐標(biāo)怠益,但頻率只有 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>
本章小結(jié)
事件處理器可以檢測并響應(yīng)發(fā)生在我們的 Web 頁面上的事件瘾婿。addEventListener
方法用于注冊處理器蜻牢。
每個(gè)事件都有標(biāo)識(shí)事件的類型(keydown
、focus
等)偏陪。大多數(shù)方法都會(huì)在特定 DOM 元素上調(diào)用抢呆,接著向其父節(jié)點(diǎn)傳播,允許每個(gè)父元素的處理器都能處理這些事件笛谦。
JavaScript 調(diào)用事件處理器時(shí)抱虐,會(huì)傳遞一個(gè)包含事件額外信息的事件對(duì)象。該對(duì)象也有方法支持停止進(jìn)一步傳播(stopPropagation
)饥脑,也支持阻止瀏覽器執(zhí)行事件的默認(rèn)處理器(preventDefault
)恳邀。
按下鍵盤按鍵時(shí)會(huì)觸發(fā)keydown
和keyup
事件。按下鼠標(biāo)按鈕時(shí)灶轰,會(huì)觸發(fā)mousedown
谣沸、mouseup
和click
事件。移動(dòng)鼠標(biāo)會(huì)觸發(fā)mousemove
事件框往。觸摸屏交互會(huì)導(dǎo)致"touchstart"
鳄抒,"touchmove"
和"touchend"
事件。
我們可以通過scroll
事件監(jiān)測滾動(dòng)行為椰弊,可以通過focus
和blur
事件監(jiān)控焦點(diǎn)改變许溅。當(dāng)文檔完成加載后,會(huì)觸發(fā)窗口的load
事件秉版。
習(xí)題
氣球
編寫一個(gè)顯示氣球的頁面(使用氣球 emoji贤重,\ud83c\udf88
)。 當(dāng)你按下上箭頭時(shí)清焕,它應(yīng)該變大(膨脹)10%并蝗,而當(dāng)你按下下箭頭時(shí),它應(yīng)該縮薪胀住(放氣)10%滚停。
您可以通過在其父元素上設(shè)置font-size
CSS 屬性(style.fontSize
)來控制文本大小(emoji 是文本)粥惧。 請(qǐng)記住在該值中包含一個(gè)單位键畴,例如像素(10px
)。
箭頭鍵的鍵名是"ArrowUp"
和"ArrowDown"
。確保按鍵只更改氣球起惕,而不滾動(dòng)頁面涡贱。
實(shí)現(xiàn)了之后,添加一個(gè)功能惹想,如果你將氣球吹過一定的尺寸问词,它就會(huì)爆炸。 在這種情況下嘀粱,爆炸意味著將其替換為“爆炸 emoji激挪,\ud83d\udca5
”,并且移除事件處理器(以便您不能使爆炸變大變胁菽隆)灌灾。
<p>💥</p>
<script>
// Your code here
</script>
鼠標(biāo)軌跡
在 JavaScript 早期,有許多主頁都會(huì)在頁面上使用大量的動(dòng)畫悲柱,人們想出了許多該語言的創(chuàng)造性用法锋喜。
其中一種是“鼠標(biāo)蹤跡”,也就是一系列的元素豌鸡,隨著你在頁面上移動(dòng)鼠標(biāo)嘿般,它會(huì)跟著你的鼠標(biāo)指針。
在本習(xí)題中實(shí)現(xiàn)鼠標(biāo)軌跡的功能涯冠。使用絕對(duì)定位炉奴、固定尺寸的<div>
元素,背景為黑色(請(qǐng)參考鼠標(biāo)點(diǎn)擊一節(jié)中的示例)蛇更。創(chuàng)建一系列此類元素瞻赶,當(dāng)鼠標(biāo)移動(dòng)時(shí),伴隨鼠標(biāo)指針顯示它們派任。
有許多方案可以實(shí)現(xiàn)我們所需的功能砸逊。你可以根據(jù)你的需要實(shí)現(xiàn)簡單的或復(fù)雜的方法。簡單的解決方案是保存固定鼠標(biāo)的軌跡元素并循環(huán)使用它們掌逛,每次mousemove
事件觸發(fā)時(shí)將下一個(gè)元素移動(dòng)到鼠標(biāo)當(dāng)前位置师逸。
<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>
選項(xiàng)卡
選項(xiàng)卡面板廣泛用于用戶界面。它支持用戶通過選擇元素上方的很多突出的選項(xiàng)卡來選擇一個(gè)面板豆混。
本習(xí)題中篓像,你必須實(shí)現(xiàn)一個(gè)簡單的選項(xiàng)卡界面耘婚。編寫asTabs
函數(shù)明垢,接受一個(gè) DOM 節(jié)點(diǎn)并創(chuàng)建選項(xiàng)卡界面來展現(xiàn)該節(jié)點(diǎn)的子元素。該函數(shù)應(yīng)該在頂層節(jié)點(diǎn)中插入大量<button>
元素逆日,與每個(gè)子元素一一對(duì)應(yīng)鸵鸥,按鈕文本從子節(jié)點(diǎn)的data-tabname
中獲取奠滑。除了顯示一個(gè)初始子節(jié)點(diǎn),其他子節(jié)點(diǎn)都應(yīng)該隱藏(將display
樣式設(shè)置成none
),并通過點(diǎn)擊按鈕來選擇當(dāng)前顯示的節(jié)點(diǎn)养叛。
當(dāng)它生效時(shí)將其擴(kuò)展,為當(dāng)前選中的選項(xiàng)卡宰翅,將按鈕的樣式設(shè)為不同的弃甥,以便明確選擇了哪個(gè)選項(xiàng)卡。
<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>