擷取和浮現讓我們得以實作其中一種最強大的事件處理模式,稱為「事件委派」。
其概念是,如果我們有許多元素以類似的方式處理,那麼我們會將單一處理常式放在它們的共同祖先上,而不是為每個元素指定一個處理常式。
在處理常式中,我們會取得 event.target
來查看事件實際發生在哪裡,並加以處理。
讓我們來看一個範例,八卦圖反映了古老的中國哲學。
如下所示
HTML 如下所示
<table>
<tr>
<th colspan="3"><em>Bagua</em> Chart: Direction, Element, Color, Meaning</th>
</tr>
<tr>
<td class="nw"><strong>Northwest</strong><br>Metal<br>Silver<br>Elders</td>
<td class="n">...</td>
<td class="ne">...</td>
</tr>
<tr>...2 more lines of this kind...</tr>
<tr>...2 more lines of this kind...</tr>
</table>
表格有 9 個儲存格,但可以有 99 或 9999 個,這並不重要。
我們的任務是點擊時突顯一個儲存格 <td>
。
我們不會為每個 <td>
(可能很多)指定一個 onclick
處理常式,而是會在 <table>
元素上設定「全域」處理常式。
它會使用 event.target
來取得被點擊的元素並突顯它。
程式碼
let selectedTd;
table.onclick = function(event) {
let target = event.target; // where was the click?
if (target.tagName != 'TD') return; // not on TD? Then we're not interested
highlight(target); // highlight it
};
function highlight(td) {
if (selectedTd) { // remove the existing highlight if any
selectedTd.classList.remove('highlight');
}
selectedTd = td;
selectedTd.classList.add('highlight'); // highlight the new td
}
這樣的程式碼不關心表格中有多少個儲存格。我們可以隨時動態新增/移除 <td>
,突顯功能仍然會正常運作。
不過,還是有一個缺點。
點擊可能不是發生在 <td>
上,而是在其內部。
在我們的案例中,如果我們查看 HTML 內部,我們可以看到 <td>
內部有巢狀標籤,例如 <strong>
<td>
<strong>Northwest</strong>
...
</td>
自然地,如果點擊發生在那個 <strong>
上,那麼它就會變成 event.target
的值。
在處理常式 table.onclick
中,我們應該取得這樣的 event.target
,並找出點擊是否發生在 <td>
內部。
以下是改良後的程式碼
table.onclick = function(event) {
let td = event.target.closest('td'); // (1)
if (!td) return; // (2)
if (!table.contains(td)) return; // (3)
highlight(td); // (4)
};
說明
- 方法
elem.closest(selector)
會傳回符合選擇器的最近祖先。在我們的案例中,我們從來源元素向上尋找<td>
。 - 如果
event.target
不在任何<td>
內部,那麼呼叫會立即傳回,因為沒有任何事要做。 - 在巢狀表格的情況下,
event.target
可能是一個<td>
,但位於目前表格外部。因此,我們會檢查它是否實際上是我們表格的<td>
。 - 如果是的話,就突顯它。
結果,我們有一個快速、有效率的突顯程式碼,它不關心表格中 <td>
的總數。
委派範例:標記中的動作
事件委派還有其他用途。
假設我們想要建立一個包含「儲存」、「載入」、「搜尋」等按鈕的選單。而且有一個包含 save
、load
、search
… 等方法的物件。要如何比對它們?
第一個想法可能是為每個按鈕指定一個獨立的處理常式。但有一個更優雅的解決方案。我們可以為整個選單新增一個處理常式,並為具有要呼叫方法的按鈕新增 data-action
屬性
<button data-action="save">Click to Save</button>
處理常式會讀取屬性並執行方法。查看運作範例
<div id="menu">
<button data-action="save">Save</button>
<button data-action="load">Load</button>
<button data-action="search">Search</button>
</div>
<script>
class Menu {
constructor(elem) {
this._elem = elem;
elem.onclick = this.onClick.bind(this); // (*)
}
save() {
alert('saving');
}
load() {
alert('loading');
}
search() {
alert('searching');
}
onClick(event) {
let action = event.target.dataset.action;
if (action) {
this[action]();
}
};
}
new Menu(menu);
</script>
請注意,this.onClick
在 (*)
中繫結到 this
。這很重要,因為否則其內的 this
會參照 DOM 元素(elem
),而不是 Menu
物件,而 this[action]
就會不是我們需要的東西。
那麼,委派在此處帶給我們什麼優勢?
- 我們不需要撰寫程式碼來為每個按鈕指定處理常式。只要建立一個方法並將其放入標記中即可。
- HTML 結構很靈活,我們可以隨時新增/移除按鈕。
我們也可以使用類別 .action-save
、.action-load
,但屬性 data-action
在語意上會更好。我們也可以在 CSS 規則中使用它。
「行為」模式
我們也可以使用事件委派來為元素新增「行為」,並使用特殊屬性和類別以「宣告式」的方式進行。
此模式包含兩個部分
- 我們為元素新增一個自訂屬性來描述其行為。
- 一個文件層級的處理常式會追蹤事件,如果事件發生在具有屬性的元素上,則會執行動作。
行為:計數器
例如,這裡的屬性 data-counter
會為按鈕新增一個行為:「按一下就增加值」
Counter: <input type="button" value="1" data-counter>
One more counter: <input type="button" value="2" data-counter>
<script>
document.addEventListener('click', function(event) {
if (event.target.dataset.counter != undefined) { // if the attribute exists...
event.target.value++;
}
});
</script>
如果我們按一下按鈕,其值就會增加。這裡重要的是按鈕,而不是一般的方法。
可以有任意數量的屬性具有 data-counter
。我們可以隨時在 HTML 中新增新的屬性。使用事件委派,我們「擴充」了 HTML,新增了一個屬性來描述新的行為。
addEventListener
當我們為 document
物件指定事件處理常式時,我們應該永遠使用 addEventListener
,而不是 document.on<event>
,因為後者會造成衝突:新的處理常式會覆寫舊的處理常式。
對於實際專案,由程式碼的不同部分設定在 document
上的處理常式很多,這是很正常的。
行為:切換器
另一個行為的範例。按一下具有屬性 data-toggle-id
的元素會顯示/隱藏具有指定 id
的元素
<button data-toggle-id="subscribe-mail">
Show the subscription form
</button>
<form id="subscribe-mail" hidden>
Your mail: <input type="email">
</form>
<script>
document.addEventListener('click', function(event) {
let id = event.target.dataset.toggleId;
if (!id) return;
let elem = document.getElementById(id);
elem.hidden = !elem.hidden;
});
</script>
讓我們再次注意我們所做的。現在,要為元素新增切換功能,不需要知道 JavaScript,只要使用屬性 data-toggle-id
即可。
這可能會變得非常方便,不需要為每個這樣的元素撰寫 JavaScript。只要使用行為即可。文件層級的處理常式會讓它對頁面的任何元素都起作用。
我們也可以在單一元素上結合多個行為。
「行為」模式可以作為 JavaScript 小型片段的替代方案。
摘要
事件委派真的很酷!它是 DOM 事件中最有用的模式之一。
它通常用於為許多類似的元素添加相同的處理,但不僅於此。
演算法
- 在容器上放置一個單一處理常式。
- 在處理常式中 - 檢查來源元素
event.target
。 - 如果事件發生在我們感興趣的元素內部,則處理事件。
好處
- 簡化初始化並節省記憶體:無需添加許多處理常式。
- 更少的程式碼:在新增或移除元素時,無需新增/移除處理常式。
- DOM 修改:我們可以使用
innerHTML
等大量新增/移除元素。
當然,委派有其限制
- 首先,事件必須是冒泡的。有些事件不會冒泡。此外,低階處理常式不應使用
event.stopPropagation()
。 - 其次,委派可能會增加 CPU 負載,因為容器層級處理常式會對容器中任何地方的事件做出反應,無論我們是否感興趣。但通常負載可以忽略不計,因此我們不考慮它。
留言
<code>
標籤,對於多行程式碼,請將其包覆在<pre>
標籤中,對於超過 10 行的程式碼,請使用沙盒 (plnkr、jsbin、codepen…)