最近在深刻實踐js中,遇到了一些問題,好比我須要爲動態建立的DOM元素綁定事件,那麼普通的事件綁定就不行了,因而經過上網查資料瞭解到事件委託,所以想總結一下js中的事件綁定與事件委託。html
以下示例代碼,經過節點屬性顯式聲明,直接在HTML中,顯式地爲按鈕綁定了click事件,當該按鈕有用戶點擊行爲時,便會觸發myClickFunc方法。node
/* html */ <button id="btn" onclick="myClickFunc()"> ClickMe </button> /* js */ // 事件處理程序 var myClickFunc = function(evt){ // TODO.. }; // 移除事件處理程序 myClickFunc = function(){};
顯而易見,這種綁定方式很是不友好,HTML代碼和JS代碼嚴重耦合在一塊兒,好比當要修改一個函數名時候,就要修改兩次,web
經過DOM操做動態綁定事件,是一種比較傳統的方式,把一個函數賦值給事件處理程序。這種方式也是應用較多的方式,比較簡單。看下面例子:瀏覽器
/* html */ <button id="btn">ClickMe</button> /* js */ // 事件處理程序 var myClickFunc = function(evt){ // TODO ... }; // 直接給DOM節點的 onclick 方法賦值,注意這裏接收的是一個function document.getElementById('btn').onclick = myClickFunc; // 移除事件處理程序 document.getElementById('btn').onclick = null;
經過事件監聽的方式綁定事件,DOM2級事件定義了兩個方法,用於處理指定和刪除事件處理程序的操做。函數
// event: 事件名稱 // function: 事件函數 // boolean: false | true, true 爲事件捕獲, false 爲事件冒泡(默認); Ele.addEventListener(event,function[,boolean]); // 添加句柄 ELe.removeEventListener(event,function[,boolean]); // 移除句柄
看個例子:this
/* html */ <button id="btn">ClickMe</button> /* js */ // 經過DOM操做進行動態綁定: // 獲取btnHello節點 var oBtn = document.getElementById('btn'); // 增長第一個 click 事件監聽處理程序 oBtn.addEventListener('click',function(evt){ // TODO sth 1... }); // 增長第二個 click 事件監聽處理程序 oBtn.addEventListener('click',function(evt){ // TODO sth 2... }); // ps:經過這種形式,能夠給btn按鈕綁定任意多個click監聽;注意,執行順序與添加順序相關。 // 移除事件處理程序 oBtn.removeEventListener('click',function(evt){..});
DOM 2級事件處理程序在IE是行不通的,IE有本身的事件處理程序方法:attachEvent()
和detachEvent()
。這兩個方法的用法與addEventListener()
是同樣的,可是隻接收兩個參數,一個是事件名稱,另外一個是事件處理程序的函數。爲何不使用第三個參數的緣由呢?由於IE8以及更早的瀏覽器版本只支持事件冒泡。看個例子:spa
/* html */ <button id="btn">ClickMe</button> /* js */ var oBtn = document.getElementById('btn'); // 事件處理函數 function evtFn(){ console.log(this); } // 添加句柄 oBtn.attachEvent('onclick',evtFn); // 移除句柄 oBtn.detachEvent('onclick',evtFn);
若是咱們既要支持IE的事件處理方法,又要支持 DOM 2級事件,那麼就要封裝一個跨瀏覽器的事件處理函數,若是支持 DOM 2級事件,就用addEventListener
,不然就用attachEvent
。例子以下:.net
//跨瀏覽器事件處理程序 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; } } }; var oBtn = document.getElementById('btn'); function evtFn(){ alert('hello world'); } eventUtil.addHandler(oBtn, 'click', evtFn); eventUtil.removeHandler(oBtn, 'click', evtFn);
在瞭解事件委託以前,要先了解下事件冒泡和事件捕獲。code
早期的web開發,瀏覽器廠商很難回答一個哲學上的問題:當你在頁面上的一個區域點擊時,你真正感興趣的是哪一個元素。這個問題帶來了交互的定義。在一個元素的界限內點擊,顯得有點含糊。畢竟,在一個元素上的點擊同時也發生在另外一個元素的界限內。例如單擊一個按鈕。你實際上點擊了按鈕區域、body元素的區域以及html元素的區域。htm
伴隨着這個問題,兩種主流的瀏覽器Netscape和IE有不一樣的解決方案。Netscape定義了一種叫作事件捕獲的處理方法,事件首先發生在DOM樹的最高層對象(document)而後往最深層的元素傳播。在圖例中,事件捕獲首先發生在document上,而後是html元素,body元素,最後是button元素。
IE的處理方法正好相反。他們定義了一種叫事件冒泡的方法。事件冒泡認爲事件促發的最深層元素首先接收事件。而後是它的父元素,依次向上,知道document對象最終接收到事件。儘管相對於html元素來講,document沒有獨立的視覺表現,他仍然是html元素的父元素而且事件能冒泡到document元素。因此圖例中噢噢那個button元素先接收事件,而後是body、html最後是document。以下圖:
簡單點說,事件冒泡就是事件觸發時,會從目標DOM元素向上傳播,直到文檔根節點,通常狀況下,會是以下形式傳播:
targetDOM → parentNode → ... → body → document → window
若是但願一次事件觸發能在整個DOM樹上都獲得響應,那麼就須要用到事件冒泡的機制。看下面示例:
/* html */ <button id="btn">ClickMe</button> /* js */ // 給按鈕增長click監聽 document.getElementById('btn').addEventListener('click',function(evt){ alert('button clicked'); },false); // 給body增長click監聽 document.body.addEventListener('click',function(evt){ alert('body clicked'); },false);
在這種狀況下,點擊按鈕「ClickMe」後,其自身的click事件會被觸發,同時,該事件將會繼續向上傳播, 全部的祖先節點都將獲得事件的觸發命令,並當即觸發本身的click事件;因此如上代碼,將會連續彈出兩個alert.
在有些時候,咱們想讓事件獨立觸發,因此咱們必須阻止冒泡,用event
的stopPropagation()
方法。
<button id="btn">ClickMe</button> /* js */ // 給按鈕增長click監聽 document.getElementById('btn').addEventListener('click',function(evt){ alert('button clicked'); evt.stopPropagation(); //阻止事件冒泡 },false); // 給body增長click監聽 document.body.addEventListener('click',function(evt){ alert('body clicked'); },false);
此時,點擊按鈕後,只會觸發按鈕自己的click事件,獲得一個alert效果;該按鈕的點擊事件,不會向上傳播,body節點就接收不到這次事件命令。
須要注意的是:
不是全部的事件都能冒泡,如:blur、focus、load、unload都不能
不一樣的瀏覽器,阻止冒泡的方式也不同,在w3c標準中,經過event.stopPropagation()
完成, 在IE中則是經過自身的event.cancelBubble=true
來完成。
事件委託看起來挺難理解,可是舉個生活的例子。好比,有三個同事預計會在週一收到快遞。爲簽收快遞,有兩種辦法:一是三我的在公司門口等快遞;二是委託給前臺MM代爲簽收。現實當中,咱們大都採用委託的方案(公司也不會容忍那麼多員工站在門口就爲了等快遞)。前臺MM收到快遞後,她會判斷收件人是誰,而後按照收件人的要求籤收,甚至代爲付款。這種方案還有一個優點,那就是即便公司裏來了新員工(無論多少),前臺MM也會在收到寄給新員工的快遞後覈實並代爲簽收。舉個例子
HTML結構:
<ul id="ul-item"> <li>item1</li> <li>item2</li> <li>item3</li> <li>item4</li> </ul>
若是咱們要點擊li標籤,彈出裏面的內容,咱們就須要爲每一個li標籤綁定事件。
(function(){ var oUlItem = document.getElementById('ul-item'); var oLi = oUlItem.getElementsByTagName('li'); for(var i=0, l = oLi.length; i < l; i++){ oLi[i].addEventListener('click',show); }; function show(e){ e = e || window.event; alert(e.target.innerHTML); }; })();
雖然這樣子可以實現咱們想要的功能,可是若是這個UL中的LI子元素頻繁的添加或刪除,咱們就須要在每次添加LI的時候爲它綁定事件。這就添加了複雜度,而且形成內存開銷較大。
更簡單的方法是利用事件委託,當事件被掏到更上層的父節點的時候,經過檢查事件的目標對象(target)來判斷並獲取事件源LI。
(function(){ var oUlItem = document.getElementById('ul-item'); oUlItem.addEventListener('click',show); function show(e){ e = e || window.event; var src = e.target; if(src && src.nodeName.toLowerCase() === 'li'){ alert(src.innerHTML); } } })();
這裏咱們爲父節點UL添加了點擊事件,當點擊子節點LI標籤的時候,點擊事件會冒泡到父節點。父節點捕獲到事件以後,經過判斷e.target.nodeName
來判斷是否爲咱們須要處理的節點,而且經過e.target
拿到了被點擊的Li節點。從而能夠獲取到相應的信息,並作處理。
優勢:
經過上面的介紹,你們應該可以體會到使用事件委託對於web應用程序帶來的幾個優勢:
管理的函數變少了。不須要爲每一個元素都添加監聽函數。對於同一個父節點下面相似的子元素,能夠經過委託給父元素的監聽函數來處理事件。
能夠方便地動態添加和修改元素,不須要由於元素的改動而修改事件綁定。
JavaScript和DOM節點之間的關聯變少了,這樣也就減小了因循環引用而帶來的內存泄漏發生的機率。