事件 是某事發(fā)生的信號(hào)。所有的 DOM 節(jié)點(diǎn)都生成這樣的信號(hào)(但事件不僅限于 DOM)。
這是最有用的 DOM 事件的列表,你可以瀏覽一下:
鼠標(biāo)事件:
click
? —— 當(dāng)鼠標(biāo)點(diǎn)擊一個(gè)元素時(shí)(觸摸屏設(shè)備會(huì)在點(diǎn)擊時(shí)生成)。contextmenu
? —— 當(dāng)鼠標(biāo)右鍵點(diǎn)擊一個(gè)元素時(shí)。mouseover
? / ?mouseout
? —— 當(dāng)鼠標(biāo)指針移入/離開一個(gè)元素時(shí)。mousedown
? / ?mouseup
? —— 當(dāng)在元素上按下/釋放鼠標(biāo)按鈕時(shí)。mousemove
? —— 當(dāng)鼠標(biāo)移動(dòng)時(shí)。鍵盤事件:
keydown
? 和 ?keyup
? —— 當(dāng)按下和松開一個(gè)按鍵時(shí)。表單(form)元素事件:
submit
? —— 當(dāng)訪問者提交了一個(gè) ?<form>
? 時(shí)。focus
? —— 當(dāng)訪問者聚焦于一個(gè)元素時(shí),例如聚焦于一個(gè) ?<input>
?。Document 事件:
DOMContentLoaded
? —— 當(dāng) HTML 的加載和處理均完成,DOM 被完全構(gòu)建完成時(shí)。CSS 事件:
transitionend
? —— 當(dāng)一個(gè) CSS 動(dòng)畫完成時(shí)。還有很多其他事件。我們將在下一章中詳細(xì)介紹具體事件。
為了對(duì)事件作出響應(yīng),我們可以分配一個(gè) 處理程序(handler)—— 一個(gè)在事件發(fā)生時(shí)運(yùn)行的函數(shù)。
處理程序是在發(fā)生用戶行為(action)時(shí)運(yùn)行 JavaScript 代碼的一種方式。
有幾種分配處理程序的方法。讓我們來看看,從最簡(jiǎn)單的開始。
處理程序可以設(shè)置在 HTML 中名為 on<event>
的特性(attribute)中。
例如,要為一個(gè) input
分配一個(gè) click
處理程序,我們可以使用 onclick
,像這樣;
<input value="Click me" onclick="alert('Click!')" type="button">
在鼠標(biāo)點(diǎn)擊時(shí),onclick
中的代碼就會(huì)運(yùn)行。
請(qǐng)注意,在 onclick
中,我們使用單引號(hào),因?yàn)樘匦员旧硎褂玫氖请p引號(hào)。如果我們忘記了代碼是在特性中的,而使用了雙引號(hào),像這樣:onclick="alert("Click!")"
,那么它就無法正確運(yùn)行。
HTML 特性不是編寫大量代碼的好位置,因此我們最好創(chuàng)建一個(gè) JavaScript 函數(shù),然后在 HTML 特性中調(diào)用這個(gè)函數(shù)。
我們知道,HTML 特性名是大小寫不敏感的,所以 ONCLICK
和 onClick
以及 onCLICK
都一樣可以運(yùn)行。但是特性通常是小寫的:onclick
。
我們可以使用 DOM 屬性(property)on<event>
來分配處理程序。
例如 elem.onclick
:
<input id="elem" type="button" value="Click me">
<script>
elem.onclick = function() {
alert('Thank you');
};
</script>
如果一個(gè)處理程序是通過 HTML 特性(attribute)分配的,那么隨后瀏覽器讀取它,并從特性的內(nèi)容創(chuàng)建一個(gè)新函數(shù),并將這個(gè)函數(shù)寫入 DOM 屬性(property)。
因此,這種方法實(shí)際上與前一種方法相同。
這兩段代碼工作相同:
<input type="button" onclick="alert('Click!')" value="Button">
<input type="button" id="button" value="Button">
<script>
button.onclick = function() {
alert('Click!');
};
</script>
在第一個(gè)例子中,button.onclick
是通過 HTML 特性(attribute)初始化的,而在第二個(gè)例子中是通過腳本初始化的。這是它們唯一的不同之處。
因?yàn)檫@里只有一個(gè) onclick
屬性,所以我們無法分配更多事件處理程序。
在下面這個(gè)示例中,我們使用 JavaScript 添加了一個(gè)處理程序,覆蓋了現(xiàn)有的處理程序:
<input type="button" id="elem" onclick="alert('Before')" value="Click me">
<script>
elem.onclick = function() { // 覆蓋了現(xiàn)有的處理程序
alert('After'); // 只會(huì)顯示此內(nèi)容
};
</script>
要移除一個(gè)處理程序 —— 賦值 elem.onclick = null
。
處理程序中的 this
的值是對(duì)應(yīng)的元素。就是處理程序所在的那個(gè)元素。
下面這行代碼中的 button
使用 this.innerHTML
來顯示它的內(nèi)容:
<button onclick="alert(this.innerHTML)">Click me</button>
如果你剛開始寫事件 —— 請(qǐng)注意一些細(xì)微之處。
我們可以將一個(gè)現(xiàn)存的函數(shù)用作處理程序:
function sayThanks() {
alert('Thanks!');
}
elem.onclick = sayThanks;
但要注意:函數(shù)應(yīng)該是以 sayThanks
的形式進(jìn)行賦值,而不是 sayThanks()
。
// 正確
button.onclick = sayThanks;
// 錯(cuò)誤
button.onclick = sayThanks();
如果我們添加了括號(hào),那么 sayThanks()
就變成了一個(gè)函數(shù)調(diào)用。所以,最后一行代碼實(shí)際上獲得的是函數(shù)執(zhí)行的 結(jié)果,即 undefined
(因?yàn)檫@個(gè)函數(shù)沒有返回值)。此代碼不會(huì)工作。
……但在標(biāo)記(markup)中,我們確實(shí)需要括號(hào):
<input type="button" id="button" onclick="sayThanks()">
這個(gè)區(qū)別很容易解釋。當(dāng)瀏覽器讀取 HTML 特性(attribute)時(shí),瀏覽器將會(huì)使用 特性中的內(nèi)容 創(chuàng)建一個(gè)處理程序。
所以,標(biāo)記(markup)會(huì)生成下面這個(gè)屬性:
button.onclick = function() {
sayThanks(); // <-- 特性(attribute)中的內(nèi)容變到了這里
};
不要對(duì)處理程序使用 setAttribute
。
這樣的調(diào)用會(huì)失效:
// 點(diǎn)擊 <body> 將產(chǎn)生 error,
// 因?yàn)樘匦钥偸亲址?,函?shù)變成了一個(gè)字符串
document.body.setAttribute('onclick', function() { alert(1) });
DOM 屬性是大小寫敏感的。
將處理程序分配給 elem.onclick
,而不是 elem.ONCLICK
,因?yàn)?DOM 屬性是大小寫敏感的。
上述分配處理程序的方式的根本問題是 —— 我們不能為一個(gè)事件分配多個(gè)處理程序。
假設(shè),在我們點(diǎn)擊了一個(gè)按鈕時(shí),我們代碼中的一部分想要高亮顯示這個(gè)按鈕,另一部分則想要顯示一條消息。
我們想為此事件分配兩個(gè)處理程序。但是,新的 DOM 屬性將覆蓋現(xiàn)有的 DOM 屬性:
input.onclick = function() { alert(1); }
// ...
input.onclick = function() { alert(2); } // 替換了前一個(gè)處理程序
Web 標(biāo)準(zhǔn)的開發(fā)者很早就了解到了這一點(diǎn),并提出了一種使用特殊方法 addEventListener
和 removeEventListener
來管理處理程序的替代方法。它們沒有這樣的問題。
添加處理程序的語法:
element.addEventListener(event, handler[, options]);
?event
?
事件名,例如:?"click"
?。
?handler
?
處理程序。
?options
?
具有以下屬性的附加可選對(duì)象:
once
?:如果為 ?true
?,那么會(huì)在被觸發(fā)后自動(dòng)刪除監(jiān)聽器。capture
?:事件處理的階段,我們稍后將在 冒泡和捕獲 一章中介紹。由于歷史原因,?options
? 也可以是 ?false/true
?,它與 ?{capture: false/true}
? 相同。passive
?:如果為 ?true
?,那么處理程序?qū)⒉粫?huì)調(diào)用 ?preventDefault()
?,我們稍后將在 瀏覽器默認(rèn)行為 一章中介紹。要移除處理程序,可以使用 removeEventListener
:
element.removeEventListener(event, handler[, options]);
移除需要相同的函數(shù)
要移除處理程序,我們需要傳入與分配的函數(shù)完全相同的函數(shù)。
這不起作用:
elem.addEventListener( "click" , () => alert('Thanks!')); // .... elem.removeEventListener( "click", () => alert('Thanks!'));
處理程序不會(huì)被移除,因?yàn)?nbsp;
removeEventListener
獲取了另一個(gè)函數(shù) —— 使用相同的代碼,但這并不起作用,因?yàn)樗且粋€(gè)不同的函數(shù)對(duì)象。
下面是正確方法:
function handler() { alert( 'Thanks!' ); } input.addEventListener("click", handler); // .... input.removeEventListener("click", handler);
請(qǐng)注意 —— 如果我們不將函數(shù)存儲(chǔ)在一個(gè)變量中,那么我們就無法移除它。由
addEventListener
分配的處理程序?qū)o法被“讀回”。
多次調(diào)用 addEventListener
允許添加多個(gè)處理程序,如下所示:
<input id="elem" type="button" value="Click me"/>
<script>
function handler1() {
alert('Thanks!');
};
function handler2() {
alert('Thanks again!');
}
elem.onclick = () => alert("Hello");
elem.addEventListener("click", handler1); // Thanks!
elem.addEventListener("click", handler2); // Thanks again!
</script>
正如我們?cè)谏厦孢@個(gè)例子中所看到的,我們可以 同時(shí) 使用 DOM 屬性和 addEventListener
來設(shè)置處理程序。但通常我們只使用其中一種方式。
對(duì)于某些事件,只能通過 ?
addEventListener
? 設(shè)置處理程序有些事件無法通過 DOM 屬性進(jìn)行分配。只能使用
addEventListener
。
例如,
DOMContentLoaded
事件,該事件在文檔加載完成并且 DOM 構(gòu)建完成時(shí)觸發(fā)。
// 永遠(yuǎn)不會(huì)運(yùn)行 document.onDOMContentLoaded = function() { alert("DOM built"); };
// 這種方式可以運(yùn)行 document.addEventListener("DOMContentLoaded", function() { alert("DOM built"); });
所以
addEventListener
更通用。雖然這樣的事件是特例而不是規(guī)則。
為了正確處理事件,我們需要更深入地了解發(fā)生了什么。不僅僅是 “click” 或 “keydown”,還包括鼠標(biāo)指針的坐標(biāo)是什么?按下了哪個(gè)鍵?等等。
當(dāng)事件發(fā)生時(shí),瀏覽器會(huì)創(chuàng)建一個(gè) event
對(duì)象,將詳細(xì)信息放入其中,并將其作為參數(shù)傳遞給處理程序。
下面是一個(gè)從 event
對(duì)象獲取鼠標(biāo)指針的坐標(biāo)的示例:
<input type="button" value="Click me" id="elem">
<script>
elem.onclick = function(event) {
// 顯示事件類型、元素和點(diǎn)擊的坐標(biāo)
alert(event.type + " at " + event.currentTarget);
alert("Coordinates: " + event.clientX + ":" + event.clientY);
};
</script>
event
對(duì)象的一些屬性:
?event.type
?
事件類型,這里是 ?"click"
?。
?event.currentTarget
?
處理事件的元素。這與 ?this
? 相同,除非處理程序是一個(gè)箭頭函數(shù),或者它的 ?this
? 被綁定到了其他東西上,之后我們就可以從 ?event.currentTarget
? 獲取元素了。
?event.clientX
? / ?event.clientY
?
指針事件(pointer event)的指針的窗口相對(duì)坐標(biāo)。
還有很多屬性。其中很多都取決于事件類型:鍵盤事件具有一組屬性,指針事件具有另一組屬性,稍后我們將詳細(xì)討論不同事件,那時(shí)我們?cè)賹?duì)其進(jìn)行詳細(xì)研究。
?
event
? 對(duì)象在 HTML 處理程序中也可用如果我們?cè)?HTML 中分配了一個(gè)處理程序,那么我們也可以使用
event
對(duì)象,像這樣:
<input type="button" onclick="alert(event.type)" value="Event type">
這是可能的,因?yàn)楫?dāng)瀏覽器讀取特性(attribute)時(shí),它會(huì)創(chuàng)建像這樣的處理程序:
function(event) { alert(event.type) }
。也就是說:它的第一個(gè)參數(shù)是"event"
,而主體取自于該特性(attribute)。
我們不僅可以分配函數(shù),還可以使用 addEventListener
將一個(gè)對(duì)象分配為事件處理程序。當(dāng)事件發(fā)生時(shí),就會(huì)調(diào)用該對(duì)象的 handleEvent
方法。
例如:
<button id="elem">Click me</button>
<script>
let obj = {
handleEvent(event) {
alert(event.type + " at " + event.currentTarget);
}
};
elem.addEventListener('click', obj);
</script>
正如我們所看到的,當(dāng) addEventListener
接收一個(gè)對(duì)象作為處理程序時(shí),在事件發(fā)生時(shí),它就會(huì)調(diào)用 obj.handleEvent(event)
來處理事件。
我們也可以對(duì)此使用一個(gè)類:
<button id="elem">Click me</button>
<script>
class Menu {
handleEvent(event) {
switch(event.type) {
case 'mousedown':
elem.innerHTML = "Mouse button pressed";
break;
case 'mouseup':
elem.innerHTML += "...and released.";
break;
}
}
}
let menu = new Menu();
elem.addEventListener('mousedown', menu);
elem.addEventListener('mouseup', menu);
</script>
這里,同一個(gè)對(duì)象處理兩個(gè)事件。請(qǐng)注意,我們需要使用 addEventListener
來顯式設(shè)置事件,以指明要監(jiān)聽的事件。這里的 menu
對(duì)象只監(jiān)聽 mousedown
和 mouseup
,而沒有任何其他類型的事件。
handleEvent
方法不必通過自身完成所有的工作。它可以調(diào)用其他特定于事件的方法,例如:
<button id="elem">Click me</button>
<script>
class Menu {
handleEvent(event) {
// mousedown -> onMousedown
let method = 'on' + event.type[0].toUpperCase() + event.type.slice(1);
this[method](event);
}
onMousedown() {
elem.innerHTML = "Mouse button pressed";
}
onMouseup() {
elem.innerHTML += "...and released.";
}
}
let menu = new Menu();
elem.addEventListener('mousedown', menu);
elem.addEventListener('mouseup', menu);
</script>
現(xiàn)在事件處理程序已經(jīng)明確地分離了出來,這樣更容易進(jìn)行代碼編寫和后續(xù)維護(hù)。
這里有 3 種分配事件處理程序的方式:
onclick="..."
?。elem.onclick = function
?。elem.addEventListener(event, handler[, phase])
? 用于添加,?removeEventListener
? 用于移除。HTML 特性很少使用,因?yàn)?HTML 標(biāo)簽中的 JavaScript 看起來有些奇怪且陌生。而且也不能在里面寫太多代碼。
DOM 屬性用起來還可以,但我們無法為特定事件分配多個(gè)處理程序。在許多場(chǎng)景中,這種限制并不嚴(yán)重。
最后一種方式是最靈活的,但也是寫起來最長(zhǎng)的。有少數(shù)事件只能使用這種方式。例如 transtionend
和 DOMContentLoaded
(上文中講到了)。addEventListener
也支持對(duì)象作為事件處理程序。在這種情況下,如果發(fā)生事件,則會(huì)調(diào)用 handleEvent
方法。
無論你如何分類處理程序 —— 它都會(huì)將獲得一個(gè)事件對(duì)象作為第一個(gè)參數(shù)。該對(duì)象包含有關(guān)所發(fā)生事件的詳細(xì)信息。
在下一章中,我們將學(xué)習(xí)更多關(guān)于一般事件和不同類型事件的內(nèi)容。
為 button
添加 JavaScript 代碼,使得 <div id="text">
在我們點(diǎn)擊該按鈕時(shí)消失。
創(chuàng)建一個(gè)按鈕,在被點(diǎn)擊時(shí),隱藏自己。
可以在處理程序中使用 this
來引用“元素自身”:
<input type="button" onclick="this.hidden=true" value="Click to hide">
在變量中有一個(gè)按鈕。它上面沒有處理程序。
執(zhí)行以下代碼之后,哪些處理程序會(huì)在按鈕被點(diǎn)擊時(shí)運(yùn)行?會(huì)顯示哪些 alert?
button.addEventListener("click", () => alert("1"));
button.removeEventListener("click", () => alert("1"));
button.onclick = () => alert(2);
答案:1
和 2
。
第一個(gè)處理程序會(huì)觸發(fā),因?yàn)樗鼪]有被 removeEventListener
移除。要移除處理程序,我們需要傳遞正確的所分配的函數(shù)。在代碼中,傳遞了一個(gè)新的函數(shù),該函數(shù)看起來相同,但仍然是另一個(gè)函數(shù)。
要移除一個(gè)函數(shù)對(duì)象,我們需要存儲(chǔ)對(duì)它的引用,像這樣:
function handler() {
alert(1);
}
button.addEventListener("click", handler);
button.removeEventListener("click", handler);
無論 addEventListener
怎樣,button.onclick
處理程序都會(huì)觸發(fā)。
點(diǎn)擊球場(chǎng)中任意一點(diǎn),讓球在球場(chǎng)中移動(dòng)。
要求:
注意:
event.clientX/event.clientY
? 屬性來獲取點(diǎn)擊坐標(biāo)。首先,我們需要選擇一種定位球的方法。
我們不能使用 position:fixed
,因?yàn)闈L動(dòng)頁面會(huì)造成球被移出球場(chǎng)。
所以我們應(yīng)該使用 position:absolute
,并且要使定位真正可靠,應(yīng)該使 field
自身具有 position:absolute
。
然后,球?qū)⑾鄬?duì)于球場(chǎng)定位:
#field {
width: 200px;
height: 150px;
position: relative;
}
#ball {
position: absolute;
left: 0; /* 相對(duì)于最接近的祖先(field) */
top: 0;
transition: 1s all; /* left/top 的 CSS 動(dòng)畫,使球飛起來 */
}
接下來我們需要指定正確的 ball.style.left/top
。它們現(xiàn)在包含相對(duì)于球場(chǎng)的坐標(biāo)。
這是示意圖:
我們有 event.clientX/clientY
—— 點(diǎn)擊位置的窗口相對(duì)坐標(biāo)。
要獲取點(diǎn)擊位置的球場(chǎng)相對(duì)坐標(biāo) left
,我們可以減去球場(chǎng)左邊緣和邊框的寬度:
let left = event.clientX - fieldCoords.left - field.clientLeft;
通常情況下,ball.style.left
表示“元素的左邊緣”(球)。因此,如果我們將其指定為 left
,那么球的邊緣而非球的中心將位于鼠標(biāo)光標(biāo)下方。
我們需要將球向左移動(dòng)球?qū)挾鹊囊话?,向上移?dòng)球高度的一半,以使其居中。
所以,最后的 left
將是:
let left = event.clientX - fieldCoords.left - field.clientLeft - ball.offsetWidth/2;
使用相同的邏輯來計(jì)算垂直坐標(biāo)。
請(qǐng)注意,球的寬度/高度必須在我們?cè)L問 ball.offsetWidth
時(shí)就已知。應(yīng)該在 HTML 或 CSS 中指定。
創(chuàng)建一個(gè)在點(diǎn)擊時(shí)打開/折疊的菜單:
P.S. 源文檔的 HTML/CSS 將被修改。
首先,讓我們創(chuàng)建 HTML/CSS。
菜單是頁面上的一個(gè)獨(dú)立圖形組件,所以最好把它放入一個(gè)單獨(dú)的 DOM 元素中。
菜單項(xiàng)的列表可以被作為列表 ul/li
列出。
下面是示例結(jié)構(gòu):
<div class="menu">
<span class="title">Sweeties (click me)!</span>
<ul>
<li>Cake</li>
<li>Donut</li>
<li>Honey</li>
</ul>
</div>
我們對(duì)標(biāo)題使用 <span>
,因?yàn)?nbsp;<div>
有一個(gè)隱式的 display:block
,它會(huì)占據(jù) 100% 的水平寬度。
就像這樣:
<div style="border: solid red 1px" onclick="alert(1)">Sweeties (click me)!</div>
因此,如果我們?cè)谒厦嬖O(shè)置 onclick
,那么它也會(huì)捕獲文本右側(cè)的點(diǎn)擊。
……由于 <span>
有一個(gè)隱式的 display: inline
,它恰好占據(jù)了足以容納所有文本的位置:
<span style="border: solid red 1px" onclick="alert(1)">Sweeties (click me)!</span>
切換菜單應(yīng)更改箭頭并顯示/隱藏菜單列表。
所有這些更改都可以通過 CSS 完美處理。在 JavaScript 中,我們應(yīng)該通過添加/移除 .open
類來標(biāo)記菜單的當(dāng)前狀態(tài)。
沒有它,菜單就會(huì)被關(guān)閉:
.menu ul {
margin: 0;
list-style: none;
padding-left: 20px;
display: none;
}
.menu .title::before {
content: '? ';
font-size: 80%;
color: green;
}
……有 .open
后,箭頭會(huì)改變,列表會(huì)出現(xiàn):
.menu.open .title::before {
content: '▼ ';
}
.menu.open ul {
display: block;
}
有一個(gè)消息列表。
使用 JavaScript 在每條消息的右上角添加一個(gè)關(guān)閉按鈕。
結(jié)果應(yīng)該如下所示:
我們可以使用 position:absolute
(并使窗格 position:relative
)或者 float:right
來添加按鈕。float:right
的好處是按鈕永遠(yuǎn)都不會(huì)與文本重疊,但是 position:absolute
則提供了更大的自由度。選擇權(quán)在你自己手上。
然后,對(duì)于每個(gè)窗格(pane),代碼可以像這樣:
pane.insertAdjacentHTML("afterbegin", '<button class="remove-button">[x]</button>');
然后 <button>
變成了 pane.firstChild
,因此我們可以像這樣為它添加處理程序:
pane.firstChild.onclick = () => pane.remove();
重要程度: 4
創(chuàng)建一個(gè)“輪播圖(carousel)” —— 一條可以通過點(diǎn)擊箭頭來滾動(dòng)圖像的圖像帶。
之后,我們可以為其添加更多功能:無限滾動(dòng),動(dòng)態(tài)加載等。
P.S. 對(duì)于這個(gè)任務(wù),HTML/CSS 結(jié)構(gòu)實(shí)際上占解決方案的 90%。
圖像帶可以表示為圖像 <img>
的 ul/li
列表。
通常,這樣的圖像帶是很寬的,但我們?cè)谄渲車胖昧艘粋€(gè)固定大小的 <div>
來“剪切”它,因此,只有圖像帶的一部分是可見的:
為了使列表水平顯示,我們需要為 <li>
應(yīng)用正確的 CSS 屬性,例如 display: inline-block
。
對(duì)于 <img>
來說,我們應(yīng)該調(diào)整 display
,因?yàn)槟J(rèn)情況下它是 inline
。在 inline
元素下方為 “l(fā)etter tails” 保留了額外的空間,因此,我們可以使用 display:block
來將其刪除。
我們可以移動(dòng) <ul>
來進(jìn)行滾動(dòng)。有很多方法可以實(shí)現(xiàn)這一點(diǎn),例如,通過修改 margin-left
或者使用 transform: translateX()
(性能更好):
外部的 <div>
具有固定的寬度,因此,會(huì)裁剪掉“多余”的圖像。
整個(gè)輪播圖是頁面上的一個(gè)獨(dú)立的“圖形組件”,因此我們最好將其包裝到一個(gè)單獨(dú)的 <div class="carousel">
中,并在其中對(duì)其進(jìn)行樣式設(shè)置。
更多建議: