學(xué)這個的時候,這東西把我搞得很頭暈。于是決定寫一篇Blog出來把這玩意說清楚,讓和我一樣在這個坑里打滾的人不那么暈菜。盡量照顧到每個細(xì)節(jié),標(biāo)準(zhǔn)是讓自己再看的時候不管忘記了多少知識點(diǎn)都能看得懂。
那么,開始吧。
基礎(chǔ)知識
1.鼠標(biāo)點(diǎn)擊。
我的鼠標(biāo)左鍵點(diǎn)擊了網(wǎng)頁,然后網(wǎng)頁獲取到了我的點(diǎn)擊事件,調(diào)取了DOMAPI——真的是這樣嗎?
不,不是這樣的。鼠標(biāo)首先應(yīng)該是系統(tǒng)級的API,所以最先知情的應(yīng)該是系統(tǒng)。系統(tǒng)得知你點(diǎn)擊了鼠標(biāo),并把這個事件傳遞給瀏覽器,然后瀏覽器才會通過DOMAPI通知網(wǎng)頁。這很重要。
2.事件綁定
DOM的事件元素綁定其實(shí)沒啥好說的,無非就是綁定的方法不同。一種是直接綁定,就是xx.onclick這樣的。簡單粗暴,DOM level0就支持。等等,什么是DOM level0?這又引出了一個隱藏知識點(diǎn)。DOM level0 指的是DOM level1事件之前即支持的事件,DOM level1時間基準(zhǔn)為W3C制定第一個標(biāo)準(zhǔn)。另外一種,就是DOM level2新增的事件監(jiān)聽,見下文。
3.child元素和parent元素的通知問題。
根據(jù)1,那么我點(diǎn)擊了child元素并且child元素被監(jiān)聽,那么就會出現(xiàn)一個通知順序的問題。是父元素先通知,還是子元素先通知?那么,就順理成章的進(jìn)入了下一個介紹:
捕獲和冒泡
要說捕獲和冒泡,得先介紹事件監(jiān)聽。事件監(jiān)聽和綁定其實(shí)差不了太多,但是新增了一個特點(diǎn),就是無論監(jiān)聽多少次,都不會覆蓋掉前面的事件。講白了就是事件綁定plus。而捕獲和冒泡其實(shí)本質(zhì)上只是Child和Parent通知的兩種順序。捕獲指的是parent先通知,child后通知,而冒泡則相反.
捕獲只有在早期的瀏覽器中才使用,第一個嘗試冒泡的,居然是現(xiàn)在被批判為封建保守的IE。有因有果,這都是有故事的啊~
一大堆概念解釋:
事件捕獲:當(dāng)某個元素觸發(fā)某個事件(如onclick),頂層對象document就會發(fā)出一個事件流,隨著DOM樹的節(jié)點(diǎn)向目標(biāo)元素節(jié)點(diǎn)流去,直到到達(dá)事件真正發(fā)生的目標(biāo)元素。在這個過程中,事件相應(yīng)的監(jiān)聽函數(shù)是不會被觸發(fā)的。
事件目標(biāo):當(dāng)?shù)竭_(dá)目標(biāo)元素之后,執(zhí)行目標(biāo)元素該事件相應(yīng)的處理函數(shù)。如果沒有綁定監(jiān)聽函數(shù),那就不執(zhí)行。
事件冒泡:從目標(biāo)元素開始,往頂層元素傳播。途中如果有節(jié)點(diǎn)綁定了相應(yīng)的事件處理函數(shù),這些函數(shù)都會被一次觸發(fā)。
有些情況下,捕獲/冒泡并不被我們所需要,比如調(diào)試。那么我們可以使用e.stopPropagation()(Firefox)或者e.cancelBubble=true(IE)來阻止。e是什么?DOM Event。當(dāng)然,我們也能通過改變addEventListener的第三個參數(shù)改變事件的執(zhí)行順序。(false為冒泡階段執(zhí)行,true為捕獲階段執(zhí)行,默認(rèn)為false)我想,大概永遠(yuǎn)都用不到True了吧....
事件委托(事件代理)
介紹完上面的,事件委托是時候登場了。事件委托簡單說起來就是利用事件冒泡,只指定一個事件處理程序,就可以管理某一類型的所有事件。
網(wǎng)上我找了很多篇博客,基本都是用這個例子,我就直接抄過來了:
有三個同事預(yù)計(jì)會在周一收到快遞。為簽收快遞,有兩種辦法:一是三個人在公司門口等快遞;二是委托給前臺MM代為簽收。現(xiàn)實(shí)當(dāng)中,我們大都采用委托的方案,前臺MM收到快遞后,她會判斷收件人是誰,然后按照收件人的要求簽收,甚至代為付款。這種方案還有一個優(yōu)勢,那就是即使公司里來了新員工(不管多少),前臺MM也會在收到寄給新員工的快遞后核實(shí)并代為簽收。
嗯,我大概明白是啥意思了。來個例子加深一下印象吧。
<ul id="ul1">
<li>111</li>
<li>222</li>
<li>333</li>
<li>444</li>
</ul>
window.onload = function(){
var oUl = document.getElementById("ul1");
var aLi = oUl.getElementsByTagName('li');
for(var i=0;i<aLi.length;i++){
aLi[i].onclick = function(){
alert(123);
}
}
}
這是一段傳統(tǒng)的的DOM操作遍歷li實(shí)現(xiàn)事件操作的代碼。有用,但是很蠢。遍歷每一個li?真的有這個必要嗎?
聰明的人已經(jīng)想到了,那么我監(jiān)聽ul不就好了嗎!bingo,那么用事件委托監(jiān)聽ul會怎么樣呢?
window.onload = function(){
var oUl = document.getElementById("ul1");
oUl.onclick = function(){
alert(123);
}
嗯,點(diǎn)擊ul確實(shí)已經(jīng)實(shí)現(xiàn)了功能。但是出現(xiàn)了一個bug.當(dāng)我們點(diǎn)擊li之外,ul之內(nèi),事件一樣觸發(fā)了。因?yàn)槭录墙壎ㄔ趗l上的??!也許通過控制ul的樣式能夠解決這個問題,但是并不是一個好辦法。嗯,其實(shí)寫個判斷就能夠解決了。這里有個需要解釋的e.什么是e?DOM Event.它提供了一個屬性叫target,可以返回事件的目標(biāo)節(jié)點(diǎn)。有個小坑:webkit等瀏覽器一般是使用e.target,而IE瀏覽器要用event.srcElement.而且這個target只是獲取節(jié)點(diǎn)位置,我們還需要用nodeName獲取標(biāo)簽名,并轉(zhuǎn)化為小寫做比較。
ul.addEventListener('click', function(e) {
// 檢查事件源e.targe是否為Li
if (e.target && e.target.nodeName.toUpperCase == "LI") {
// 真正的處理過程在這里
console.log("123");
}
}
這樣就只有點(diǎn)擊li會觸發(fā)事件了,并且只執(zhí)行一次dom操作。絕大多數(shù)的blog也是這么寫的。
但是,它有bug!
如果第一個li外面套了一個span呢?
<ul id="ul1">
<span><li>111</li></span>
<li>222</li>
<li>333</li>
<li>444</li>
</ul>
再次點(diǎn)擊第一個li,它居然不觸發(fā)!為什么?console大法一下,發(fā)現(xiàn)我們點(diǎn)擊的居然是span.那么這個循壞就錯的徹徹底底了。但是其實(shí)做個小小的修改就行了。
ul.addEventListener('click', function() {
let el = e.target
while (el && !el.matches(selector)) {
el = el.parentNode
if (element === el) {
el = null
}
}
if (el) {
console.log('123')
}
}
OK,寫寫抄抄終于總結(jié)完了,自己算是弄懂了,下次有機(jī)會就用起來試試~