一文梳理JS事件
- 2020 年 6 月 17 日
- 筆記
- javascript, 前端
JavaScript與HTML的交互是通過事件進行的。事件,就是文檔或瀏覽器窗口發生的一些特定的交互瞬間。
- 事件流
- 事件捕獲
- 事件冒泡
- 事件處理程序
- 事件委託
1. 事件流
如果單機頁面上的某個按鈕,認為單擊事件不僅僅發生在按鈕上。火炬話說,在單擊按鈕的同時,你也單擊的按鈕的容器元素,甚至也單擊了整個頁面。
事件流描述的是從頁面中接收事件的順序。有意思的是,IE和Netscape開發團隊提出了幾乎完全相反的事件流的概念。IE的事件流是事件冒泡流,而Netscape的事件流是事件捕獲流。
1.1 事件冒泡
IE的事件流叫做事件冒泡,即時間開始時由最具體的元素(文檔中嵌套層次最深的那個節點)接收,然後逐級向上傳播到較為不具體的節點(文檔)。以下面的HTML頁面為例:
<!DOCTYPE>
<html>
<head>
<title> test </title>
</head>
<body>
<div id="myDiv">click me</div>
</body>
</html>
在這裡,如果你單擊了頁面中的div元素,那麼click事件會按照如下順序傳播:
- div
- body
- html
- document
如圖所示:
所有的現代瀏覽器都支持事件冒泡,但在具體實現中略有差別.IE5.5
及更早版本中事件冒泡會跳過<html>
元素(從body直接跳到document)。IE9
、Firefox、Chrome、和Safari則將事件一直冒泡到window對象。
1.2 事件捕獲
Netscape團隊提出的另一種事件流叫做事件捕獲。思想是:不大具體的節點應該更早接收到事件,而最具體的節點應該最後接收到事件。事件捕獲的用意在於事件到達預定目標之前捕獲它。仍以上面的栗子,單擊div元素就會以下列順序觸發click事件。
- document
- html
- body
- div
具體如圖所示:
事件捕獲流在IE9
,Safari
,Chrome
,Opera
和FireFox
都得到支持,儘管DOM標準要求事件應該從document對象開始傳播,但這些瀏覽器都是從window對象開始捕獲事件的。由於老版本瀏覽器不支持,很少有人使用事件捕獲。建議使用事件冒泡。
1.3 DOM事件流
「DOM2級事件」規定事件流包括三個階段:事件捕獲階段,處於目標階段和事件冒泡階段。首先發生的是事件捕獲,為截獲事件提供了機會。然後是實際的目標接收到事件。最後一個階段是冒泡階段,可以在這個階段對事件做出響應。還是之前的栗子,如圖所示:
多數支持 DOM事件流的瀏覽器都實現了一種特定的行為;即使「DOM2
級事件」規範明確要求捕
獲階段不會涉及事件目標,但 IE9
、Safari
、Chrome
、Firefox
和 Opera 9.5
及更高版本都會在捕獲階段觸發事件對象上的事件。結果,就是有兩個機會在目標對象上面操作事件。
注意:IE9
、Safari
、Chrome
、Firefox
和 Opera
都支持 DOM 事件流;IE8
及更早版本不支持 DOM 事件流。
2. 事件處理程序
事件就是用戶或瀏覽器自身執行的某種動作。諸如 click 、 load 和 mouseover ,都是事件的名字。
而響應某個事件的函數就叫做事件處理程序(或事件偵聽器)。
2.1 HTML事件處理程序
某個元素支持的每種事件,都可以使用一個與相應事件處理程序同名的 HTML 特性來指定。這個
特性的值應該是能夠執行的 JavaScript 代碼。例如,要在按鈕被單擊時執行一些 JavaScript,可以像下面這樣編寫代碼:
<input type="button" value="Click Me" onclick="alert('Clicked')" />
當單擊這個按鈕時,就會顯示一個警告框。這個操作是通過指定 onclick
特性並將一些 JavaScript
代碼作為它的值來定義的。
2.2 DOM 0級 事件處理程序
通過 JavaScript 指定事件處理程序的傳統方式,就是將一個函數賦值給一個事件處理程序屬性,為所有現代瀏覽器所支持。
每個元素(包括 window 和 document )都有自己的事件處理程序屬性,這些屬性通常全部小寫,
例如 onclick 。將這種屬性的值設置為一個函數,就可以指定事件處理程序,如下所示:
var btn = document.getElementById("myBtn");
btn.onclick = function(){
alert("Clicked");
};
在此,我們通過文檔對象取得了一個按鈕的引用,然後為它指定了 onclick
事件處理程序。但要
注意,在這些代碼運行以前不會指定事件處理程序,因此如果這些代碼在頁面中位於按鈕後面,就有可能在一段時間內怎麼單擊都沒有反應。
使用 DOM0 級方法指定的事件處理程序被認為是元素的方法。因此,這時候的事件處理程序是在
元素的作用域中運行;換句話說,程序中的 this 引用當前元素。
2.3 DOM 2級事件處理程序
「DOM2
級事件」定義了兩個方法,用於處理指定和刪除事件處理程序的操作:addEventListener()
和 removeEventListener()
。所有 DOM 節點中都包含這兩個方法,並且它們都接受 3 個參數:要處理的事件名、作為事件處理程序的函數和一個布爾值。最後這個布爾值參數如果是 true ,表示在捕獲階段調用事件處理程序;如果是 false ,表示在冒泡階段調用事件處理程序。
var btn = document.getElementById("myBtn");
btn.addEventListener("click", function(){
alert(this.id);
}, false);
上面的代碼為一個按鈕添加了 onclick
事件處理程序,而且該事件會在冒泡階段被觸發(因為最
後一個參數是 false )。與DOM0
級方法一樣,這裡添加的事件處理程序也是在其依附的元素的作用域中運行。使用 DOM2
級方法添加事件處理程序的主要好處是可以添加多個事件處理程序。
通過addEventListener()
添加的事件處理程序只能使用removeEventListener()
來移除;移
除時傳入的參數與添加處理程序時使用的參數相同。這也意味着通過 addEventListener()
添加的匿名函數將無法移除
注意:IE9、Firefox、Safari、Chrome和 Opera 支持 DOM2 級事件處理程序。
2.4 IE事件處理程序
IE 實現了與 DOM 中類似的兩個方法: attachEvent()
和 detachEvent()
。這兩個方法接受相同
的兩個參數:事件處理程序名稱與事件處理程序函數。由於IE8
及更早版本只支持事件冒泡,所以通過attachEvent()
添加的事件處理程序都會被添加到冒泡階段。
要使用 attachEvent() 為按鈕添加一個事件處理程序,可以使用以下代碼。
var btn = document.getElementById("myBtn");
btn.attachEvent("onclick", function(){
alert("Clicked");
});
在 IE 中使用 attachEvent()
與使用 DOM0
級方法的主要區別在於事件處理程序的作用域。在使
用DOM0
級方法的情況下,事件處理程序會在其所屬元素的作用域內運行;在使用 attachEvent()
方法的情況下,事件處理程序會在全局作用域中運行,因此 this 等於 window 。
使用 attachEvent()
添加的事件可以通過 detachEvent()
來移除,條件是必須提供相同的參數。
與 DOM 方法一樣,這也意味着添加的匿名函數將不能被移除。不過,只要能夠將對相同函數的引用傳給 detachEvent()
,就可以移除相應的事件處理程序。
2.5 跨瀏覽器的事件處理程序
為了以跨瀏覽器的方式處理事件,不少開發人員會使用能夠隔離瀏覽器差異的 JavaScript 庫,還有
一些開發人員會自己開發最合適的事件處理的方法。自己編寫代碼其實也不難,只要恰當地使用能力檢測即可。要保證處理事件的代碼能在大多數瀏覽器下一致地運行,只需關注冒泡階段。
第一個要創建的方法是 addHandler()
,它的職責是視情況分別使用 DOM0
級方法、DOM2
級方
法或 IE 方法來添加事件。這個方法屬於一個名叫 EventUtil
的對象,本書將使用這個對象來處理瀏覽器間的差異。 addHandler()
方法接受 3 個參數:要操作的元素、事件名稱和事件處理程序函數。
與 addHandler()
對應的方法是 removeHandler()
,它也接受相同的參數。這個方法的職責是移
除之前添加的事件處理程序——無論該事件處理程序是採取什麼方式添加到元素中的,如果其他方法無效,默認採用 DOM0
級方法。
var EventUtil = {
addHandler: function(element, type, handler){
if (element.addEventListener){
element.addEventListener(type, handler, false);
} else if (element.attachEvent){
element.attachEvent("on" + type, handler);
} else {
element["on" + type] = handler;
}
},
removeHandler: function(element, type, handler){
if (element.removeEventListener){
element.removeEventListener(type, handler, false);
} else if (element.detachEvent){
element.detachEvent("on" + type, handler);
} else {
element["on" + type] = null;
}
}
};
addHandler()
和 removeHandler()
沒有考慮到所有的瀏覽器問題,例如在 IE 中的作用域問題。
不過,使用它們添加和移除事件處理程序還是足夠了。此外還要注意,DOM0
級對每個事件只支持一個事件處理程序。
3. 事件委託
3.1 什麼是事件委託?
事件委託,也稱事件代理(Event Delegation),是JS中常用綁定事件的技巧。事件委託就是利用事件冒泡,只指定一個事件處理程序,就可以管理某一類型的所有事件。
3.2 為什麼要使用事件委託?
眾所周知,瀏覽器的內存是有限的,如果很多的dom需要添加事件處理,直接給相應dom設事件處理程序的話,十分影響性能。
可以想像,如果有100個li,每個li都有相同的click事件,首先我們需要使用for循環訪問所有li元素,然後給他們添加事件,這樣,會使得訪問dom的次數增加,其次,每個li元素一個函數,佔用內存也會相當大。每一個函數都是對象,對象越多佔用內存越大,性能越差。同時,訪問dom次數的越多,引起瀏覽器重繪與重排的次數也就越多,就會延長整個頁面的交互就緒時間,這就是為什麼性能優化的主要思想之一就是減少DOM操作的原因;如果要用事件委託,就會將所有的操作放到js程序裏面,與dom的操作就只需要交互一次,這樣就能大大的減少與dom的交互次數,提高性能。
3.3 事件委託原理
事件委託是利用事件的冒泡原理來實現,將事件加到 父元素 或 祖先元素上,觸發執行效果。
3.4 事件委託實現案例
如下所示,需要實現點擊li元素,彈出123:
<ul id="list">
<li>111</li>
<li>222</li>
<li>333</li>
<li>444</li>
</ul>
一般方法:
window.onload = function(){
var list = document.getElementById("list");
var lists = list.getElementsByTagName('li');
for(var i=0;i<lists.length;i++){
lists[i].onclick = function(){
alert(123);
}
}
}
事件委託
window.onload = function(){
var list = document.getElementById("list");
list.onclick = function() {
alert(123);
}
}
5. 小結
事件是將Javascript與網頁聯繫在一起的主要方式。為了將JS事件進行梳理,本文一一闡述了事件冒泡,事件捕獲,DOM事件流,循序漸進的將事件流這一脈絡梳理;然後,將事件處理程序包括HTML事件處理程序、DOM0級事件處理程序、IE事件處理程序、DOM2級事件處理程序等一一列舉,並給出了跨瀏覽器事件處理方案;最後,講述了事件委託這一JS中常用技巧,並列舉了使用這一技術的好處。希望在閱讀本文後,讀者對於JS事件有一個清晰的認知。