關於JS中事件代理的解析

Mentionlyze發表於2019-02-16

概述

一般來說,我們在為前端頁面設計互動的的時候往往需要為DOM元素新增事件處理程式。但是很多時候頁面的DOM元素的結構和層級會很複雜,如果我們為所有需要新增事件處理的DOM元素一一繫結上事件處理程式,那麼不僅編寫出的程式碼會很繁雜,整個頁面的效能也會很低下。比如我們有一個有序或者無序的列表,其中包裹了數百個子節點li,一般來說,在通過選擇器拿到元素集合後,我們會用for迴圈對集合進行遍歷,然後為其新增事件處理方法,這麼做會帶來什麼結果呢,在JS引擎中,程式碼操作的DOM數量和新增的事件處理程式數量直接關係到頁面的整體效能。操作的DOM數量越多,對DOM的訪問次數越多,瀏覽器的重繪次數就越來越多,頁面的響應速度和執行效能也就越來越差。所以,在進行程式優化的過程,其中一個重要的思路就是減少DOM的操作。

這時,事件代理存在的重要意義就體現出來了,它為我們提供了一種新的解決為大量類似的DOM元素新增事件處理的解決方案。只為一個容器或者說父節點新增一次事件處理程式,就達成了控制這個容器中一系列元素的目標,大大的減少了瀏覽器對DOM的訪問。

事件代理原理

事件代理本質上來說是利用事件冒泡的機制來進行實現的。何為事件冒泡呢,百科中的解釋是當事件發生後,這個事件就要開始傳播(從裡到外或者從外向裡)。為什麼要傳播呢?因為事件源本身(可能)並沒有處理事件的能力,即處理事件的函式(方法)並未繫結在該事件源上。例如我們點選一個按鈕時,就會產生一個click事件,但這個按鈕本身可能不能處理這個事件,事件必須從這個按鈕傳播出去,從而到達能夠處理這個事件的程式碼中(例如我們給按鈕的onclick屬性賦一個函式的名字,就是讓這個函式去處理該按鈕的click事件),或者按鈕的父級繫結有事件函式,當該點選事件發生在按鈕上,按鈕本身並無處理事件函式,則傳播到父級去處理。

相關實現

具體怎麼實現事件代理呢,我們來看一些簡單的相關例子:

先寫一個簡單的有序列表結構。

<ol>
    <li>1</li>
    <li>2</li>
    <li>3</li>
    <li>4</li>
    <li>5</li>
</ol>

比如我們想要實現的是點選li,彈出li的序列。如果要直接操作li。

var lists = document.querySelectorAll(`li`);
for(let i = 0; i < lists.length; i ++){
    lists[i].onclick =function () {
        alert(lists[i].innerText);
    }
}

通過選擇器拿到了li的集合,然後進行遍歷並一一繫結點選事件,如果使用事件代理來實現呢

var lists = document.querySelector(`ol`);
lists.onclick = function (event) {
    alert(event.target.innerText);
}

通過選擇器拿到li的容器也就是父節點ol,然後只繫結一次點選事件,並通過event物件提供的屬性target指向容器的子節點,然後彈出該子節點的文字內容。需要了解的是,event物件提供了兩個屬性,分別是currentTarget和target,前者指向的是繫結事件的當前節點,後者則指向當前節點的一級子節點。當我們面臨事件冒泡和事件捕獲時,這兩個屬效能夠為我們提供更高的靈活度。

上訴例子中,我們需要新增事件的DOM元素都是相同的,事件的種類也是一樣的,如果我們面對的是結構更復雜一些的DOM集合呢,來看下面的例子。

DOM結構:

<div class="wrap">
    <button>清空</button>
    <input type="submit" value="提交">
    <input type="text">
    <input type="checkbox">
</div>

在這樣的DOM結構中,我們要為四種不同的元素新增事件控制,如果只是通過target指向的話是無法實現的,需要挖掘target更深層的屬性來進行條件判斷,而target具備的屬性就是指向的子節點所具備的屬性,比如nodeName、id、className、value等

let box = document.querySelector(`.wrap`);
box.onclick = function (event) {
    if(event.target.nodeName === `button`){
        //...
    }else if(event.target.value === `提交`){
        //...
    }else if(event.target.className === `text`){
        //...
    }else{
        //...
    }
};

這樣我們同樣只在父節點上繫結一次事件就完成了我們想要的效果,無論是編碼效率和效能損耗都比不適用事件代理的情況下更加優化。我們可以發現,相比與遍歷元素集合的方式,事件代理最大的好處就是減少了DOM的操作,從來提升了效能。

適用場景

事件代理並非在所有場景中都適用的,在使用事件代理時,我們需要考慮新增事件處理的父節點能否觸發我們想要繫結的事件,比如:

<div class="wrap">
    <input type="text"><br>
    <input type="text">
</div>

當我們想為容器中的文字框繫結blur事件改變框體顏色時

let box = document.querySelector(`.wrap`);
box.addEventListener(`blur`,function (event) {
    event.target.style.borderColor = `red`;
});

我們發現這樣做是無法生效的,應為div元素是無法觸發onblur事件的,同時還有其他有關的輸入框事件也是無法觸發的,如oninput、onfocus等。

相關文章