JS事件委託

2022-03-15 19:00:22
事件委託是利用事件冒泡原理,管理某一型別的所有事件,利用父元素來代表子元素的某一型別事件的處理方式。

接下來我們通過兩種比較常見的場景來進行分析,一種是已有元素的事件繫結,另一種是新建立元素的事件繫結。

已有元素的事件繫結

場景:假如頁面上有一個 ul 標籤,裡面包含 1000 個 li 子標籤,我們需要在單擊每個 li 時,輸出 li 中的文字內容。

遇到這樣的場景時,很多人的第一想法就是給每個 li 標籤繫結一個 click 事件,在 click 事件中輸出 li 標籤的文字內容,以下是一些簡單易懂的實現方法。

HTML 程式碼

HTML 程式碼很簡單,就是一個包含很多 li 標籤的 ul 標籤,後面過多的程式碼使用省略號代替。
<ul>
    <li>文字1</li>
    <li>文字2</li>
    <li>文字3</li>
    <li>文字4</li>
    <li>文字5</li>
    <li>文字6</li>
    <li>文字7</li>
    <li>文字8</li>
    <li>文字9</li>
    ...
</ul>

JavaScript程式碼

在獲取所有的 li 標籤後,對其進行遍歷,在遍歷時新增 click 事件處理程式。
<script>
    // 1.獲取所有的li標籤
    var children = document.querySelectorAll('li');
    // 2.遍歷新增click事件處理程式
    for (var i = 0; i < children.length; i++) {
        children[i].addEventListener('click', function () {
            console.log(this.innerText);
        });
    }
</script>
當我們單擊 li 標籤時,會對應地輸出 li 標籤上的內容,如下所示:

文字1
文字6
文字9
文字7

採用上述的方法對瀏覽器的效能是一個很大的挑戰,主要包含以下兩方面原因:

  • 事件處理程式過多導致頁面互動時間過長。

假如有 1000 個 li 元素,則需要繫結 1000 個事件處理程式,而事件處理程式需要不斷地與 DOM 節點進行互動,因此引起瀏覽器重繪和重排的次數也會增多,從而會延長頁面互動時間。

  • 事件處理程式過多導致記憶體佔用過多。

在 JavaScript 中,一個事件處理程式其實就是一個函數物件,會佔用一定的記憶體空間。假如頁面有 10000 個 li 標籤,則會有 10000 個函數物件,佔用的記憶體空間會急劇上升,從而影響瀏覽器的效能。

那麼遇到這個問題時,有什麼好的解決辦法呢?答案就是利用事件委託機制。

事件委託機制的主要思想是將事件繫結至父元素上,然後利用事件冒泡原理,當事件進入冒泡階段時,通過繫結在父元素上的事件物件來判斷當前事件流正在進行的元素。如果和期望的元素相同,則執行相應的事件程式碼。

根據以上的分析,我們可以按步驟依次得到以下程式碼:
// 1.獲取父元素
var parent = document.querySelector('ul');
// 2.父元素繫結事件
parent.addEventListener('click', function (event) {
    // 3.獲取事件物件
    var event = EventUtil.getEvent(event);
    // 4.獲取目標元素
    var target = EventUtil.getTarget(event);
    // 5.判斷當前事件流所處的元素
    if (target.nodeName.toLowerCase() === 'li') {
          // 6.與目標元素相同,做對應的處理
        console.log(target.innerText);
    }
});
執行上面的程式碼,當我們單擊 li 標籤時,會得到與前面方法相同的輸出。

通過上面的程式碼可以看出,事件是繫結在父元素 ul 上的,不管子元素 li 有多少個,也不會影響到頁面中事件處理程式的個數,因此可以極大地提高瀏覽器的效能。

新建立元素的事件繫結

場景:假如頁面上有一個 ul 標籤,裡面包含 9 個 li 子標籤,我們需要在單擊每個 li 時,輸出 li 中的文字內容;在頁面上有一個 button 按鈕,單擊 button 按鈕會建立一個新的 li 元素,單擊新建立的 li 元素,輸出它的文字內容。

根據上面的場景描述,我們可以通過以下兩種方法來實現:

手動繫結方法

首先是和已有元素的事件繫結中相同的程式碼,由於邏輯是相同的,這裡就不贅述,直接給出程式碼。
<ul>
    <li>文字1</li>
    <li>文字2</li>
    <li>文字3</li>
    <li>文字4</li>
    <li>文字5</li>
    <li>文字6</li>
    <li>文字7</li>
    <li>文字8</li>
    <li>文字9</li>
</ul>

// 1.獲取所有的li標籤
var children = document.querySelectorAll('li');
// 2.遍歷新增click事件處理程式
for (var i = 0; i < children.length; i++) {
    children[i].addEventListener('click', function () {
        console.log(this.innerText);
    });
}
然後在頁面上新增一個 button 按鈕,用於新增一個 li 元素。
<button id="add">新增</button>
var ul = document.querySelector('ul');
var add = document.querySelector('#add');
add.addEventListener('click', function () {
    // 建立新的li元素
    var newLi = document.createElement('li');
    var newText = document.createTextNode('文字10');
    newLi.appendChild(newText);
    // 新增至父元素ul中
    ul.appendChild(newLi);
});
當我們單擊新增按鈕時,會發現頁面上新增了一個內容為“文字10”的 li 元素。

當我們單擊這個 li 元素時,會在控制檯輸出“文字10”嗎?

我們在瀏覽器中驗證後會發現,控制檯中沒有輸出任何內容。這是為什麼呢?

因為我們通過 querySelectorAll() 函數獲取到的 li 元素雖然會實時感知到數量的變化,但並不會實時增加對事件的繫結。如果需要新元素也具有相同的事件,則需要手動呼叫事件繫結的程式碼。

解決方案如下:

  • 將遍歷新增 click 事件處理程式程式碼封裝成一個函數。
// 遍歷新增click事件處理程式
function bindEvent() {
    for (var i = 0; i < children.length; i++) {
        children[i].addEventListener('click', function () {
            console.log(this.innerText);
        });
    }
}
  • 在新增完新元素後,重新呼叫一次上面封裝的函數。
add.addEventListener('click', function () {
    var newLi = document.createElement('li');
    var newText = document.createTextNode('文字10');
    newLi.appendChild(newText);
    ul.appendChild(newLi);
    // 重新新增事件處理程式
    bindEvent();
});
但是,通過上面的分析我們發現,每次在新增一個元素後都需要手動繫結事件處理程式,這樣的操作是很煩瑣的,而且隨著繫結的事件處理程式越來越多,效能也將受到影響。

那麼,我們有沒有什麼更好的方法呢?答案就是使用事件委託機制。

事件委託方法

使用事件委託機制,我們可以更加方便快捷地實現新建立元素的事件繫結。由於事件委託機制是利用的事件冒泡機制,即使在元素自身沒有繫結事件的情況下,事件仍然會冒泡到父元素中,因此對於新增的元素,只要處理事件流就可以觸發其事件。

針對上述問題的描述,我們需要做的就是使用事件委託機制編寫程式碼。
<script>
    // 1.獲取父元素
    var parent = document.querySelector('ul');
    // 2.父元素繫結事件
    parent.addEventListener('click', function (event) {
        // 3.獲取事件物件
        var event = EventUtil.getEvent(event);
        // 4.獲取目標元素
        var target = EventUtil.getTarget(event);
        // 5.判斷當前事件流所處的元素
        if (target.nodeName.toLowerCase() === 'li') {
            // 6.與目標元素相同,做對應的處理
            console.log(target.innerText);
        }
    });

</script>
新增按鈕的事件不變,和手動繫結方法中的一樣,這裡就不贅述。

當我們在瀏覽器中執行可以發現,新增的 li 元素在單擊後,會在控制檯輸出“文字10”,這就代表使用事件委託機制方便快捷地解決了這個問題。