最近在深入實踐js中,遇到了一些問題,比如我需要為動態建立的DOM元素繫結事件,那麼普通的事件繫結就不行了,於是通過上網查資料瞭解到事件委託,因此想總結一下js中的事件繫結與事件委託。

事件繫結

最直接的事件繫結:HTML事件處理程式

如下示例程式碼,通過節點屬性顯式宣告,直接在HTML中,顯式地為按鈕綁定了click事件,當該按鈕有使用者點選行為時,便會觸發myClickFunc方法。

/* html */
<button id="btn" onclick="myClickFunc()">
    ClickMe
</button>

/* js */
// 事件處理程式
var myClickFunc = function(evt){
    // TODO..
};

// 移除事件處理程式
myClickFunc = function(){};

顯而易見,這種繫結方式非常不友好,HTML程式碼和JS程式碼嚴重耦合在一起,比如當要修改一個函式名時候,就要修改兩次,

DOM 0 級事件處理程式

通過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;

DOM 2 級事件處理程式

通過事件監聽的方式繫結事件,DOM2級事件定義了兩個方法,用於處理指定和刪除事件處理程式的操作。

// event: 事件名稱
// function: 事件函式
// boolean: false | true, true 為事件捕獲, false 為事件冒泡(預設);
Ele.addEventListener(event,function[,boolean]); // 新增控制代碼
ELe.removeEventListener(event,function[,boolean]); // 移除控制代碼

看個例子:

/* 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){..});

IE事件處理程式

DOM 2級事件處理程式在IE是行不通的,IE有自己的事件處理程式方法:attachEvent()detachEvent()。這兩個方法的用法與addEventListener()是一樣的,但是隻接收兩個引數,一個是事件名稱,另一個是事件處理程式的函式。為什麼不使用第三個引數的原因呢?因為IE8以及更早的瀏覽器版本只支援事件冒泡。看個例子:

/* 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。例子如下:

//跨瀏覽器事件處理程式
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);

事件冒泡和事件捕獲

在瞭解事件委託之前,要先了解下事件冒泡和事件捕獲。

早期的web開發,瀏覽器廠商很難回答一個哲學上的問題:當你在頁面上的一個區域點選時,你真正感興趣的是哪個元素。這個問題帶來了互動的定義。在一個元素的界限內點選,顯得有點含糊。畢竟,在一個元素上的點選同時也發生在另一個元素的界限內。例如單擊一個按鈕。你實際上點選了按鈕區域、body元素的區域以及html元素的區域。

伴隨著這個問題,兩種主流的瀏覽器Netscape和IE有不同的解決方案。Netscape定義了一種叫做事件捕獲的處理方法,事件首先發生在DOM樹的最高層物件(document)然後往最深層的元素傳播。在圖例中,事件捕獲首先發生在document上,然後是html元素,body元素,最後是button元素。

IE的處理方法正好相反。他們定義了一種叫事件冒泡的方法。事件冒泡認為事件促發的最深層元素首先接收事件。然後是它的父元素,依次向上,知道document物件最終接收到事件。儘管相對於html元素來說,document沒有獨立的視覺表現,他仍然是html元素的父元素並且事件能冒泡到document元素。所以圖例中噢噢那個button元素先接收事件,然後是body、html最後是document。如下圖:

事件.png

事件冒泡

簡單點說,事件冒泡就是事件觸發時,會從目標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.

在有些時候,我們想讓事件獨立觸發,所以我們必須阻止冒泡,用eventstopPropagation()方法。

<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節點就接收不到此次事件命令。

需要注意的是:

  1. 不是所有的事件都能冒泡,如:blur、focus、load、unload都不能

  2. 不同的瀏覽器,阻止冒泡的方式也不一樣,在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應用程式帶來的幾個優點:

  1. 管理的函式變少了。不需要為每個元素都新增監聽函式。對於同一個父節點下面類似的子元素,可以通過委託給父元素的監聽函式來處理事件。

  2. 可以方便地動態新增和修改元素,不需要因為元素的改動而修改事件繫結。

  3. JavaScript和DOM節點之間的關聯變少了,這樣也就減少了因迴圈引用而帶來的記憶體洩漏發生的概率。

參考資料

轉載連結: