在網頁開發中,我們經常需要為 DOM 元素添加事件監聽器。但是當頁面中有大量元素需要相同的事件處理時,為每個元素單獨添加監聽器會造成性能問題。這時候就需要用到事件代理(Event Delegation)。

什麼是事件代理

事件代理是一種事件處理技術,它利用了事件冒泡的原理,將事件監聽器添加到父元素上,而不是直接添加到目標元素上。當子元素觸發事件時,事件會冒泡到父元素,父元素的監聽器就可以處理這個事件。

事件冒泡機制

在了解事件代理前,我們先來了解事件冒泡:

<div id="parent">
    <button id="child">點擊我</button>
</div>
document.getElementById('parent').addEventListener('click', () => {
    console.log('父元素被點擊');
});

document.getElementById('child').addEventListener('click', () => {
    console.log('子元素被點擊');
});

當我們點擊按鈕時,會看到:

子元素被點擊
父元素被點擊

這就是事件冒泡,事件從目標元素開始,向上冒泡到父元素。

傳統方法的問題

假設我們有一個列表,每個項目都需要點擊事件:

<ul id="list">
    <li>項目 1</li>
    <li>項目 2</li>
    <li>項目 3</li>
    <!-- 更多項目... -->
</ul>

傳統做法:

const items = document.querySelectorAll('#list li');
items.forEach(item => {
    item.addEventListener('click', handleClick);
});

function handleClick(event) {
    console.log('點擊了:', event.target.textContent);
}

這種做法的問題:

  1. 性能問題:為每個元素都添加監聽器
  2. 記憶體消耗:大量的監聽器佔用記憶體
  3. 動態元素問題:新添加的元素沒有監聽器

使用事件代理

事件代理的做法:

const list = document.getElementById('list');
list.addEventListener('click', function(event) {
    // 檢查點擊的是否為 li 元素
    if (event.target.tagName === 'LI') {
        console.log('點擊了:', event.target.textContent);
    }
});

事件代理的優點

  1. 提升性能:只需要一個監聽器
  2. 節省記憶體:減少監聽器的數量
  3. 支援動態元素:新添加的元素自動擁有事件處理
  4. 簡化代碼:統一的事件處理邏輯

實際應用範例

範例 1:動態列表

<div id="todo-app">
    <input type="text" id="new-item" placeholder="輸入新項目">
    <button id="add-btn">添加</button>
    <ul id="todo-list">
        <li>
            <span>學習 JavaScript</span>
            <button class="delete-btn">刪除</button>
        </li>
        <li>
            <span>學習 CSS</span>
            <button class="delete-btn">刪除</button>
        </li>
    </ul>
</div>
const todoList = document.getElementById('todo-list');
const newItemInput = document.getElementById('new-item');
const addBtn = document.getElementById('add-btn');

// 使用事件代理處理刪除按鈕
todoList.addEventListener('click', function(event) {
    if (event.target.classList.contains('delete-btn')) {
        // 刪除項目
        event.target.parentElement.remove();
    }
});

// 添加新項目
addBtn.addEventListener('click', function() {
    const text = newItemInput.value.trim();
    if (text) {
        const li = document.createElement('li');
        li.innerHTML = `
            <span>${text}</span>
            <button class="delete-btn">刪除</button>
        `;
        todoList.appendChild(li);
        newItemInput.value = '';
    }
});

範例 2:表格操作

<table id="data-table">
    <thead>
        <tr>
            <th>姓名</th>
            <th>年齡</th>
            <th>操作</th>
        </tr>
    </thead>
    <tbody>
        <tr>
            <td>張三</td>
            <td>25</td>
            <td>
                <button class="edit-btn" data-id="1">編輯</button>
                <button class="delete-btn" data-id="1">刪除</button>
            </td>
        </tr>
        <!-- 更多行... -->
    </tbody>
</table>
const table = document.getElementById('data-table');

table.addEventListener('click', function(event) {
    const target = event.target;
    const id = target.dataset.id;
    
    if (target.classList.contains('edit-btn')) {
        console.log('編輯 ID:', id);
        // 執行編輯邏輯
    } else if (target.classList.contains('delete-btn')) {
        console.log('刪除 ID:', id);
        // 執行刪除邏輯
        if (confirm('確定要刪除嗎?')) {
            target.closest('tr').remove();
        }
    }
});

事件代理的注意事項

  1. 事件類型限制:只能用於支援冒泡的事件(如 click、keydown 等)
  2. 目標元素檢查:需要檢查 event.target 是否為預期的元素
  3. 阻止冒泡:如果在子元素中使用 event.stopPropagation(),事件不會冒泡到父元素

進階技巧

使用 matches() 方法

list.addEventListener('click', function(event) {
    if (event.target.matches('li.item')) {
        // 處理符合選擇器的元素
        console.log('點擊了項目');
    }
});

使用 closest() 方法

list.addEventListener('click', function(event) {
    const item = event.target.closest('li');
    if (item) {
        // 處理最近的 li 祖先元素
        console.log('點擊了項目:', item.textContent);
    }
});

事件代理是一個強大的技術,能夠有效提升網頁性能,特別是在處理大量動態元素時。合理使用事件代理可以讓我們的代碼更加高效和易於維護。