JavaScript 的事件
本文作者系 360 奇舞團前端開發工程師
概述
在 Web 開發中,事件在瀏覽器窗口中被觸發並且通常被綁定到窗口內部的特定部分, 事件綁定的可能是一個元素、一系列元素或者是整個瀏覽器窗口。舉幾個可能發生的事件:
事件處理器
每個可用的事件都會有一個事件處理器(事件觸發時會運行的代碼塊),有時候事件處理器被叫做事件監聽器。
這裏通過一個簡單的例子來概況一下事件處理器的概念
e.g
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="utf-8" />
<title>Event Demo</title>
</head>
<body>
<button>press me</button>
</body>
<script>
const btn = document.querySelector("button");
function clickBtn() {
alert("點擊了按鈕");
}
btn.onclick = clickBtn;
</script>
</html>
上面script
代碼先使用Document.querySelector()
函數獲取 button 元素,然後使用 btn
變量存儲 button,然後定義了一個事件處理器函數,最後將函數(彈窗函數)賦值給 “點擊” 事件處理器參數,監聽 “點擊” 這個事件。只要點擊事件在<button>
元素上觸發,事件處理器代碼塊就會被執行。即每當用戶點擊它時,都會運行此段代碼。
觸發網頁事件的方式
內聯事件處理器
<button onclick="clickBtn()" >press me</button>
最早在 Web 上註冊事件處理程序的方法是類似於上面所示的事件處理程序 HTML 屬性,屬性值實際上是當事件發生時要運行的 JavaScript 代碼。上面的例子調用的事件處理程序在 script 模塊,但也可以直接在屬性內插入 JavaScript,例如:
<button onclick="alert('點擊了按鈕')" >press me</button>
類似 HTML 屬性等價於事件處理器屬性的方法不推薦使用 ,因爲使用一個事件處理屬性似乎看起來很簡單,但很快就變得難以管理和效率低下。
首先不應該混用 HTML 和 JavaScript,違背了職責分離的原則,會導致文檔很難解析,最好的辦法是隻在一塊地方寫 JavaScript 代碼。
即使在單一文件中,內置事件處理器也不是一個好主意。一個按鈕看起來還好,但是如果有一百個按鈕呢?您得在文件中加上 100 個屬性。這很快就會成爲維護人員的噩夢。使用 JavaScript,您可以給網頁中的 button 很方便的都加上事件處理器。就像下面這樣:
const buttons = document.querySelectorAll('button');
for (let i = 0; i < buttons.length; i++) {
buttons[i].onclick = clickBtn;
}
另外將編程邏輯與內容分離也會讓您的站點對搜索引擎更加友好。
事件處理器屬性
const btn = document.querySelector("button");
function clickBtn() {
alert("點擊了按鈕");
}
btn.onclick = clickBtn;
這個 onclick
是 button 元素的事件處理器的屬性,它就像 button 其他的屬性(如 btn.textContent
),但是有一個特別的地方——當您將一些代碼賦值給它的時候,只要事件觸發代碼就會運行。
有很多事件處理參數可供選擇,比如
-
btn.ondblclick——用於按鈕被雙擊時觸發。
-
btn.onmouseover——在鼠標移入按鈕上方時觸發
-
和 btn.onmouseout— 在鼠標從按鈕移出時觸發。
-
btn.onfocus 及 btn.onblur— 在按鈕被置於焦點或失去焦點時觸發。
一些事件非常通用,幾乎在任何地方都可以用(比如 onclick 幾乎可以用在幾乎每一個元素上),然而另一些元素就只能在特定場景下使用,比如我們只能在 video 元素上使用 onplay 。
addEventListener() 和 removeEventListener()
這個函數和事件處理器屬性是類似的,但是語法略有不同。
const btn = document.querySelector("button");
btn.addEventListener('click',(ev)=>{
alert("點擊了按鈕");
})
這個機制帶來了一些相較於舊方式的優點是有一個相對應的方法:removeEventListener(),
這個方法移除事件監聽器。例如,下面的代碼將會移除上個代碼塊中的事件監聽器:
btn.removeEventListener('click', bgChange);
在簡單的、小型的項目中可能不是很有用,但是在大型的、複雜的項目中就非常有用了,可以非常高效地清除不用的事件處理器,另外在其他的一些場景中也非常有效——比如您需要在不同環境下運行不同的事件處理器,您只需要恰當地刪除或者添加事件處理器即可。
您也可以給同一個監聽器註冊多個處理器,下面這種方式不能實現這一點:
myElement.onclick = functionA;
myElement.onclick = functionB;
第二行會覆蓋第一行,但是下面這種方式就會正常工作了:
myElement.addEventListener('click', functionA);
myElement.addEventListener('click', functionB);
當元素被點擊時兩個函數都會工作:
- 該使用哪種機制
在三種機制中,絕對不應該使用 內聯事件處理器,內聯方式嚴重違背了 HTML 和 JavaScript 職責分離的原則。
另外兩種是相對可互換的,至少對於簡單的用途:
-
事件處理器屬性具有更好的跨瀏覽器兼容性 (在 Internet Explorer 8 的支持下)。
-
addEventListener()
更強大,但是支持不足(IE9 以上纔可以使用)。
addEventListener()
的主要優點是可以使用removeEventListener()
刪除事件處理程序代碼,而且如果有需要,您可以向同一類型的元素添加多個監聽器。例如,您可以在一個元素上多次調用addEventListener('click', function() { ... })
。對於事件處理器屬性來說,這是不可能的,因爲後面任何設置的屬性都會覆蓋較早的屬性。
其他事件概念
阻止默認行爲
有時,你會遇到一些情況,你希望事件不執行它的默認行爲。最常見的例子是 Web 表單,例如自定義註冊表單。當你填寫詳細信息並按提交按鈕時,自然的行爲是將數據提交到服務器上的指定頁面進行處理,並將瀏覽器重定向到某種 “成功消息” 頁面
當用戶沒有正確提交數據時,你希望停止提交信息給服務器,並給他們一個錯誤提示,告訴他們什麼做錯了,以及需要做些什麼來修正錯誤。一些瀏覽器支持自動的表單數據驗證功能,但由於許多瀏覽器不支持,因此建議你不要依賴這些功能,還是要實現自己的驗證檢查。我們來看一個簡單的例子。
首先,一個簡單的 HTML 表單,需要你填入名(first name)和姓(last name)
<form>
<div>
<label for="fname">First name: </label>
<input id="fname" type="text">
</div>
<div>
<label for="lname">Last name: </label>
<input id="lname" type="text">
</div>
<div>
<input id="submit" type="submit">
</div>
</form>
<p></p>
這裏我們用一個onsubmit
事件處理程序來實現一個非常簡單的檢查,用於測試文本字段是否爲空。如果是,我們在事件對象上調用preventDefault()
函數,這樣就停止了表單提交,然後在我們表單下面的段落中顯示一條錯誤消息,告訴用戶什麼是錯誤的:
const form = document.querySelector('form');
const name = document.getElementById('nickName');
const pa = document.getElementById('password');
const para = document.querySelector('p');
form.onsubmit = function(e) {
if (name.value === '' || pa.value === '') {
e.preventDefault();
para.textContent = 'Please enter the user name and password!';
}
}
顯然,這是一種非常弱的表單驗證——例如,用戶輸入空格或數字提交表單,表單驗證並不會阻止用戶提交
事件冒泡和事件捕獲
當一個事件發生在具有父元素的元素上時,現代瀏覽器運行兩個不同的階段 - 捕獲階段和冒泡階段。在捕獲階段:
-
瀏覽器檢查元素的最外層祖先
<html>
,是否在捕獲階段中註冊了一個onclick
事件處理程序,如果是,則運行它。 -
然後,它移動到
<html>
中單擊元素的下一個祖先元素,並執行相同的操作,然後是單擊元素再下一個祖先元素,依此類推,直到到達實際點擊的元素。
在冒泡階段,恰恰相反:
-
瀏覽器檢查實際點擊的元素是否在冒泡階段中註冊了一個
onclick
事件處理程序,如果是,則運行它 -
然後它移動到下一個直接的祖先元素,並做同樣的事情,然後是下一個,直到它到達
<html>
元素。
在現代瀏覽器中,默認情況下,所有事件處理程序都在冒泡階段進行註冊。
- 用 stopPropagation() 阻止冒泡
標準事件對象具有可用的名爲 stopPropagation()
的函數,當在事件對象上調用該函數時,它只會讓當前事件處理程序運行,但事件不會在冒泡鏈上進一步擴大,因此將不會有更多事件處理器被運行 (不會向上冒泡)。
備註: 爲什麼我們要弄清楚捕捉和冒泡呢?那是因爲,在過去糟糕的日子裏,瀏覽器的兼容性比現在要小得多,Netscape(網景)只使用事件捕獲,而 Internet Explorer 只使用事件冒泡。當 W3C 決定嘗試規範這些行爲並達成共識時,他們最終得到了包括這兩種情況(捕捉和冒泡)的系統,最終被應用在現在瀏覽器裏。
默認情況下,所有事件處理程序都是在冒泡階段註冊的,因爲這在大多數情況下更有意義。如果您真的想在捕獲階段註冊一個事件,那麼您可以通過使用addEventListener()
註冊您的處理程序,並將可選的第三個屬性設置爲 true。
冒泡還是捕獲?
對於事件代理來說,在事件捕獲或者事件冒泡階段處理並沒有明顯的優劣之分,但是由於事件冒泡的事件流模型被所有主流的瀏覽器兼容,從兼容性角度來說還是建議大家使用事件冒泡模型。
事件委託
冒泡還允許我們利用事件委託——這個概念依賴於這樣一個事實,如果你想要在大量子元素中單擊任何一個都可以運行一段代碼,您可以將事件監聽器設置在其父節點上,並讓子節點上發生的事件冒泡到父節點上,而不是每個子節點單獨設置事件監聽器。
一個很好的例子是一系列列表項,如果你想讓每個列表項被點擊時彈出一條信息,您可以將click
單擊事件監聽器設置在父元素<ul>
上,這樣事件就會從列表項冒泡到其父元素<ul>
上。
e.g
<ul class="animal_list">
<li>pig</li>
<li>dog</li>
<li>cat</li>
<li>chicken</li>
<li>duck</li>
</ul>
<div class="box"></div>
在點擊每個 li 標籤時,輸出 li 當中的動物名(innerHTML)
。常規做法是遍歷每個 li , 然後在每個 li 上綁定一個點擊事件:
var animal_list=document.querySelector(".animal_list");
var animals=color_list.getElementsByTagName("li");
var box=document.querySelector(".box");
for(var n=0;n<colors.length;n++){
colors[n].addEventListener("click",function(){
console.log(this.innerHTML)
box.innerHTML="選擇的動物爲 "+this.innerHTML;
})
}
這種做法在 li 較少的時候可以使用,但如果有一萬個 li ,那就會導致性能降低。
這時就需要事件代理出場了,利用冒泡事件流的特性,我們只綁定一個事件處理函數也可以完成:
function animalChange(e){
var e=e||window.event;//兼容性的處理
if(e.target.nodeName.toLowerCase()==="li"){
box.innerHTML="選擇的動物爲 "+e.target.innerHTML;
}
}
color_list.addEventListener("click",colorChange,false)
由於事件冒泡機制,點擊了 li 後會冒泡到 ul ,此時就會觸發綁定在 ul 上的點擊事件,再利用 target 找到事件實際發生的元素,就可以達到預期的效果。
使用事件代理的好處不僅在於將多個事件處理函數減爲一個,而且對於不同的元素可以有不同的處理方法。假如上述列表元素當中添加了其他的元素節點(如:a、span 等),我們不必再一次循環給每一個元素綁定事件,直接修改事件代理的事件處理函數即可。
參考
https://developer.mozilla.org/zh-CN/docs/Learn/JavaScript/Building_blocks/Events
本文由 Readfog 進行 AMP 轉碼,版權歸原作者所有。
來源:https://mp.weixin.qq.com/s/VNvpV3JIJU4No4BwXzju-w