要小心 JavaScript 的事件代理
阿新 • • 發佈:2020-05-27
我們知道,如果給 form 裡面的 button 元素繫結事件,需要考慮它是否會觸發 form 的 submit 行為。除此之外,其它場合給 button 元素繫結事件,你幾乎不用擔心這個事件會有什麼非預期的附加效果,很自然地會這樣寫事件處理程式碼:
```js
var button = document.querySelector('button')
button.addEventListener('click', function (e) {
console.log('點選了按鈕')
})
```
你之所以放心這麼寫,是因為這個 button 元素沒有使用事件代理,即沒有代理任何子元素的事件。
事件代理的意思是,你要為一個元素繫結事件,但你不是直接把事件繫結到這個元素自己身上,而是繫結到這個元素的父元素上。當子元素的某個事件(比如點選事件)觸發時,它的父元素相同的事件也會觸發(我們常說的事件冒泡),此時我們說父元素代理了子元素的事件。
舉個例子,比如一個 button 元素中包含一個齒輪圖示:
```html
```
當用戶點選齒輪圖示,必然要觸發 click 事件,但你並不會直接繫結事件到 svg 或 use 元素上,而是繫結到它們的父元素 button 上。即:
```js
document.querySelector('button').addEventListener('click', function (e) {
console.log('點選了按鈕')
})
```
這種情況,我們可以說,button 元素代理了它的所有子元素的 click 事件。
但是,出現這種事件代理的情況時,我們就得小心了。
為了更直觀地說明問題,我們把“父”元素上升到頂層的 document 元素:
```js
document.documentElement.addEventListener('click', function (e) {
console.log('我被點選了')
})
```
只要網頁中任意一個位置被點選了,都會觸發繫結在 document 元素上的點選事件。 想要知道事件具體是發生在哪個元素上面,可以通過事件物件提供的 target 屬性來判斷。
```js
document.documentElement.addEventListener('click', function (e) {
console.log(e.target)
})
```
我們很容易知道事件具體是發生在哪個元素身上的。於是在上面的示例中,如果父元素 document 想在按鈕被點選時做點什麼事情,我們很自然地會這麼寫:
```js
document.documentElement.addEventListener('click', function (e) {
if (e.target.tagName === 'BUTTON') {
console.log('按鈕被點選了')
}
})
```
這時問題就出現了,按鈕即使被點選了 if 條件也不一定成立,即也不一定會輸出“按鈕被點選了”。因為使用者在按鈕上的某個位置點選了,根據使用者點選的位置,e.target 可能是下面三種情況:
- BUTTON 元素
- SVG 元素
- USE 元素
實際的情況是這樣的:
![ ](http://qa457e20e.bkt.clouddn.com/202005/25213022)
我們真正的意圖是,只要點選是發生在按鈕上面,不論是按鈕的哪個位置,我們都應視為按鈕被點選了。 嗯,簡單,我們再改一下,這樣寫:
```js
document.documentElement.addEventListener('click', function (e) {
if (['BUTTON', 'SVG', 'USE'].includes(e.target.tagName.toUpperCase())) {
// 點選的是按鈕
}
})
```
這樣似乎沒什麼問題,也確實可以達到目的,但看上去總是有些彆扭。因為這種情況對於最上層的 document 來說,得知道每個子元素的情況,本來我只需要關心離我最近的 button 元素就可以了。
根據 OOP 對內封裝的思想,button 元素內部的事情應該在內部消化掉,其子元素對外不可見,應該只暴露 button 元素本身。依據這個思想和事件冒泡的特點,我們就有了比較好的解決辦法:只需要禁止 button 內部元素的事件響應(包括事件冒泡)而只允許 button 元素本身的事件發生就行。有兩種方式可以實現這個目的。
一種是使用 CSS 禁止 button 內部元素的事件響應:
```css
button > * {
pointer-events: none;
}
```
另一種是使用 JS 來阻止 button 內部元素的事件響應(包括事件冒泡):
```js
document.querySelector('button > svg').addEventListener('click', function (e) {
e.stopPropagation()
e.preventDefault()
})
document.querySelector('button').addEventListener('click', function (e) {
console.log(e.target.tagName)
})
```
這兩種方式都能達到我們預期的效果:
![ ](http://qa457e20e.bkt.clouddn.com/202005/25225251)
綜上,針對特定元素進行事件處理時,如果該元素有事件代理的情況,就要小心處理它所代理的子元素。