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),但是有一個特別的地方——當您將一些代碼賦值給它的時候,只要事件觸發代碼就會運行。

有很多事件處理參數可供選擇,比如

一些事件非常通用,幾乎在任何地方都可以用(比如 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 職責分離的原則。

另外兩種是相對可互換的,至少對於簡單的用途:

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!';
  }
}

顯然,這是一種非常弱的表單驗證——例如,用戶輸入空格或數字提交表單,表單驗證並不會阻止用戶提交

事件冒泡和事件捕獲

當一個事件發生在具有父元素的元素上時,現代瀏覽器運行兩個不同的階段 - 捕獲階段和冒泡階段。在捕獲階段:

在冒泡階段,恰恰相反:

在現代瀏覽器中,默認情況下,所有事件處理程序都在冒泡階段進行註冊。

標準事件對象具有可用的名爲 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