JavaScript 事件委派:用父層監聽取代逐一綁定
事件委派 就是將監聽事件綁定在父層,利用 事件物件(Event Object) 來檢查目標元素是否為選取的對象,進而執行陳述事項。透過此方式,後續建立的子元素亦能執行陳述事項。
它能成立的原因只有一個:事件會 冒泡。子元素被點擊後,事件會一路往外傳,父層的監聽器自然也會被觸發。
html
<div class="box" id="Box">
<ul>
<li><a href="javascript:;"></a></li>
<li><a href="javascript:;"></a></li>
<li><a href="javascript:;"></a></li>
<li><a href="javascript:;"></a></li>
<li><a href="javascript:;"></a></li>
</ul>
</div>js
document.querySelector('#Box').addEventListener('click', (e) => {
console.log(e.target);
});css
body {
font-family: Arial, Helvetica, "微軟正黑體", sans-serif;
}
.box,
.box ul,
.box li,
.box a {
padding: 30px;
}
.box ul,
.box li,
.box a {
margin-top: 30px;
}
.box {
background-color: pink;
}
.box ul {
list-style-type: none;
margin-bottom: 0;
background-color: brown;
}
.box li {
background-color: yellowgreen;
}
.box a {
display: block;
color: #fff;
text-decoration: none;
background-color: tomato;
}
.box::before {
content: '我是 div';
}
.box ul::before {
content: '我是 ul';
color: #fff;
}
.box li::before {
content: '我是 li';
}
.box a::before {
content: '我是 a';
}把上面的範例點過一輪,就會看到 e.target 忠實回報您點到的是 <div>、<ul>、<li> 還是 <a>,而監聽器始終只有一個。
為什麼要用事件委派
另一方面,也不需要為所有的子元素加入事件監聽,讓效能浪費。
| 做法 | 監聽器數量 | 動態新增的元素 |
|---|---|---|
| 逐一綁定 | 與元素數量相同 | 必須重新綁定 |
| 事件委派 | 一個 | 自動生效 |
加上條件過濾目標
由於父層裡什麼都可能被點到,實務上一定要先判斷 target 是不是我們要的對象:
html
<div id="BtnWrap">
<input type="button" value="黑桃">
<input type="button" value="紅心">
<input type="button" value="方塊">
<input type="button" value="梅花">
</div>js
document.querySelector('#BtnWrap').addEventListener('click', (e) => {
const { target } = e;
if (target.nodeName === 'INPUT' && target.type === 'button') {
console.log(target.value);
}
});注意
nodeName 回傳的是 大寫,所以要寫 'INPUT'。這個細節在 事件物件 有詳細說明。
目標元素裡還有子元素時,用 closest()
如果按鈕裡面還包了圖示或文字,target 會是最內層那個元素,條件就對不上了。此時改用 closest() 往上找最近的祖先:
html
<ul id="List">
<li data-id="1"><span>黑桃</span></li>
<li data-id="2"><span>紅心</span></li>
</ul>js
document.querySelector('#List').addEventListener('click', (e) => {
const elemItem = e.target.closest('li');
// 點到 ul 本身時 closest 會回傳 null,先擋掉
if (!elemItem) return;
console.log(elemItem.dataset.id);
});什麼情況不適合
- 不冒泡的事件:
focus、blur、mouseenter、mouseleave。真的需要時,可改用會冒泡的focusin、focusout。 - 只有一兩個固定元素要監聽時,直接 綁在元素上 更直觀。
常見問題
事件委派為什麼能運作?
因為事件會 冒泡。子元素被點擊後,事件會往外傳到父層,父層的監聽器因此被觸發,再由事件物件的 target 得知實際被點的是哪個子元素。
事件委派有什麼好處?
一是只需要一個監聽器,不必為每個子元素各綁一次,省下記憶體與綁定成本;二是之後才動態新增的子元素不必重新綁定,同樣能被處理。
如果點到的是子元素裡面的元素怎麼辦?
此時 target 會是最內層的那個元素。用 e.target.closest() 往上找最近的目標祖先較保險,例如:closest('li')。
什麼情況不適合用事件委派?
不冒泡的事件不適合,例如:focus、blur、mouseenter、mouseleave;只有一兩個固定元素要監聽時,直接綁在元素上也更直觀。
委派時要注意 target 與 currentTarget 嗎?
要。currentTarget 永遠是綁監聽器的父層,target 才是被點到的子元素。判斷條件寫錯對象,是委派最常見的錯誤來源。