咱們知道,若是給 form 裏面的 button 元素綁定事件,須要考慮它是否會觸發 form 的 submit 行爲。除此以外,其它場合給 button 元素綁定事件,你幾乎不用擔憂這個事件會有什麼非預期的附加效果,很天然地會這樣寫事件處理代碼:css
var button = document.querySelector('button') button.addEventListener('click', function (e) { console.log('點擊了按鈕') })
你之因此放心這麼寫,是由於這個 button 元素沒有使用事件代理,即沒有代理任何子元素的事件。html
事件代理的意思是,你要爲一個元素綁定事件,但你不是直接把事件綁定到這個元素本身身上,而是綁定到這個元素的父元素上。當子元素的某個事件(好比點擊事件)觸發時,它的父元素相同的事件也會觸發(咱們常說的事件冒泡),此時咱們說父元素代理了子元素的事件。svg
舉個例子,好比一個 button 元素中包含一個齒輪圖標:3d
<button> <svg> <use xlink:href="#gear"></use> </svg> </button>
當用戶點擊齒輪圖標,必然要觸發 click 事件,但你並不會直接綁定事件到 svg 或 use 元素上,而是綁定到它們的父元素 button 上。即:代理
document.querySelector('button').addEventListener('click', function (e) { console.log('點擊了按鈕') })
這種狀況,咱們能夠說,button 元素代理了它的全部子元素的 click 事件。code
可是,出現這種事件代理的狀況時,咱們就得當心了。orm
爲了更直觀地說明問題,咱們把「父」元素上升到頂層的 document 元素:htm
document.documentElement.addEventListener('click', function (e) { console.log('我被點擊了') })
只要網頁中任意一個位置被點擊了,都會觸發綁定在 document 元素上的點擊事件。 想要知道事件具體是發生在哪一個元素上面,能夠經過事件對象提供的 target 屬性來判斷。對象
document.documentElement.addEventListener('click', function (e) { console.log(e.target) })
咱們很容易知道事件具體是發生在哪一個元素身上的。因而在上面的示例中,若是父元素 document 想在按鈕被點擊時作點什麼事情,咱們很天然地會這麼寫:blog
document.documentElement.addEventListener('click', function (e) { if (e.target.tagName === 'BUTTON') { console.log('按鈕被點擊了') } })
這時問題就出現了,按鈕即便被點擊了 if 條件也不必定成立,即也不必定會輸出「按鈕被點擊了」。由於用戶在按鈕上的某個位置點擊了,根據用戶點擊的位置,e.target 多是下面三種狀況:
實際的狀況是這樣的:
咱們真正的意圖是,只要點擊是發生在按鈕上面,不管是按鈕的哪一個位置,咱們都應視爲按鈕被點擊了。 嗯,簡單,咱們再改一下,這樣寫:
document.documentElement.addEventListener('click', function (e) { if (['BUTTON', 'SVG', 'USE'].includes(e.target.tagName.toUpperCase())) { // 點擊的是按鈕 } })
這樣彷佛沒什麼問題,也確實能夠達到目的,但看上去老是有些彆扭。由於這種狀況對於最上層的 document 來講,得知道每一個子元素的狀況,原本我只須要關心離我最近的 button 元素就能夠了。
根據 OOP 對內封裝的思想,button 元素內部的事情應該在內部消化掉,其子元素對外不可見,應該只暴露 button 元素自己。依據這個思想和事件冒泡的特色,咱們就有了比較好的解決辦法:只須要禁止 button 內部元素的事件響應(包括事件冒泡)而只容許 button 元素自己的事件發生就行。有兩種方式能夠實現這個目的。
一種是使用 CSS 禁止 button 內部元素的事件響應:
button > * { pointer-events: none; }
另外一種是使用 JS 來阻止 button 內部元素的事件響應(包括事件冒泡):
document.querySelector('button > svg').addEventListener('click', function (e) { e.stopPropagation() e.preventDefault() }) document.querySelector('button').addEventListener('click', function (e) { console.log(e.target.tagName) })
這兩種方式都能達到咱們預期的效果:
綜上,針對特定元素進行事件處理時,若是該元素有事件代理的狀況,就要當心處理它所代理的子元素。