DOM監(jiān)聽事件詳解
事件定義
? 事件并不是代碼世界里的專用詞,它僅僅是由簡單的:監(jiān)聽、變化、通知 三要素組成
? 在前端世界中,事件可以定義為:代碼監(jiān)聽(用戶),(用戶)操作產(chǎn)生變化、(程序員)得到通知。
DOM事件
如何監(jiān)聽事件
DOM level 0 事件監(jiān)聽方法: button.onclick = function(){}
這種方法是DOM level0就支持一種方法??梢杂米骱唵蔚谋O(jiān)聽。這個方法存在一個很大的問題。那就是如果一個元素綁定事件時,有可能覆蓋掉前面已經(jīng)綁定好的事件。
DOM level 2 事件監(jiān)聽方法: button.addEventListener('click', function(){})
這種方法和level 0的綁定方法是一致 ,但監(jiān)聽事件每次都會生產(chǎn)一個全新的匿名函數(shù),和前面的函數(shù)完全不同,不會覆蓋掉前面已經(jīng)綁定好的時間。
事件類型
Google: DOM events MDN
事件機制:冒泡 & 捕獲
監(jiān)聽事件中。子元素被點擊,意味著父元素也被點擊了。如果同時監(jiān)聽子元素和父元素,就會有個通知的先后順序
冒泡階段(默認使用)
事件從事件目標(target)開始,往上冒泡直到頁面的最上一級標簽。
簡單來說:就是child 先通知,parent后通知
如果想阻止事件冒泡,可以使用e.stopPropagation()(Firefox)或者e.cancelBubble=true(IE)來阻止事件的冒泡傳播。
捕獲階段
相反的,事件從最上一級標簽開始往下查找,直到捕獲到事件目標(target)。
簡單來說:就是parent 先通知,child 后通知

可以通過改變addEventListener的第三個參數(shù)改變事件的執(zhí)行順序。(false為冒泡階段執(zhí)行,true為捕獲階段執(zhí)行,留空則為false)
事件傳入屬性
事件傳入屬性:event,它傳入了事件觸發(fā)的屬性
event.addEventListener('eventType',function(e){
console.log(e)
}
如何阻止事件:
e.prevenDefault(): 阻止默認事件,可以在容器上阻止但不推薦
e.stopPropagation(): 阻止冒泡事件
使用原生 JS 實現(xiàn)事件委托
基礎:一個元素的父元素綁定了監(jiān)聽事件,而本身沒有綁定監(jiān)聽事件。如果該元素被點擊,也會觸發(fā)父元素的監(jiān)聽事件
讓我們舉個例子:
這是一個ul,里面有4個li
<ul id="ul">
<li id="li1">1</li>
<li id="li2">2</li>
<li id="li3">3</li>
<li id="li4">4</li>
</ul>
現(xiàn)在要給每個li綁定一個監(jiān)聽事件
li1.addEventListener('click', function() {})
li2.addEventListener('click', function() {})
li3.addEventListener('click', function() {})
li4.addEventListener('click', function() {})
如果執(zhí)行的函數(shù)都一樣,且li個數(shù)很多,這就顯得非常麻煩了。尤其是li個數(shù)不確定的時候,此時我們新增一個li,很顯然,新增的li并沒有綁定監(jiān)聽事件。
addButton.onclick = function(){
var li = document.createElement('li')
li.textContent = 'new'
document.querySelector('ul').appendChild(li)
}
//新增了li,但沒有自動綁定監(jiān)聽事件
但是,在此例子中,如果點擊了li,這個時候不也等于點擊了ul嗎?(參考上述基礎)
因此可以直接把點擊事件綁定在ul上
var ul = document.getElementById('ul')
ul.addEventListener('click', function() {})
那么是否就可以簡單的監(jiān)聽父元素從而達到監(jiān)聽子元素的目的呢?
這是不對的。如果給ul添加個padding。
可以看出,當點擊padding部分,也是會觸發(fā)事件的。原因是我們監(jiān)聽的是ul。

所以這種綁定法,是有bug的,這明顯不是我們想要的結(jié)果,于是我們可以給事件添加一個判斷:
判斷一下點擊的目標,如果點的是li就觸發(fā),不然就不觸發(fā)。
ul.addEventListener('click', function(e) {
// 檢查事件源e.targe是否為Li
if (e.target && e.target.nodeName.toUpperCase == "LI") {
console.log("點擊成功");
}
}
? 這里就要科普一下用到上面所說的事件傳入屬性,其中:
? target:觸發(fā)該事件時點擊的元素
? currentTarget:觸發(fā)該事件時監(jiān)聽的元素
那么,還有什么bug嗎?有的!
嘗試給li加個span試試:
<ul id="ul">
<li id="li1"><span>1</span></li> //給第一個li加一個span
<li id="li2">2</li>
<li id="li3">3</li>
<li id="li4">4</li>
</ul>
此時發(fā)現(xiàn)點擊第一個li已經(jīng)不觸發(fā)綁定事件了。
雖然前面說道,監(jiān)聽父元素就能達到監(jiān)聽子元素的目的。
但是我們?yōu)榱诵迯?‘padding’ 的bug,添加了一個標簽判斷,導致 ‘span’ 不會觸發(fā)綁定事件
if (e.target && e.target.nodeName.toUpperCase == "LI")
我們必須考慮,li是否還有后代元素。這時候我們就應該先判斷點擊的元素的祖先元素當中有沒有l(wèi)i,如果有l(wèi)i,那點擊的還是li。
所以,最后寫出的事件委托函數(shù)優(yōu)化如下
ul.addEventListener('click', function() {
let el = e.target \\獲得實際點擊的元素
\\以下while判定點擊元素el的祖先元素是否有Li
while (el && !el.matches(selector)) {
el = el.parentNode
if (element === el) {
el = null
}
}
if (el) {
console.log('執(zhí)行回調(diào)函數(shù)')
}
}