當(dāng)我們需要監(jiān)聽某個(gè)元素被點(diǎn)擊的時(shí)候,我們會(huì)給這個(gè)元素添加事件監(jiān)聽器
事件監(jiān)聽器只會(huì)綁定到當(dāng)前 DOM 中已有的元素上卵渴,而實(shí)際需求中,往往會(huì)有臨時(shí)渲染出來(lái)的元素也需要監(jiān)聽事件?那這時(shí)候改怎么辦呢撩鹿?難道在每次頁(yè)面渲染結(jié)束后,都再綁定一次事件監(jiān)聽嗎悦屏?
什么是 事件代理
從字面上來(lái)理解节沦,“代理”即將自己要做的事交給別人來(lái)做。那么這邊的“事件代理”又是什么呢础爬?同樣的甫贯,如果原本有某個(gè)事件 A 是元素 a 的事件,但是 A 事件并不直接由 a 來(lái)完成看蚜,而是轉(zhuǎn)交給元素 b 來(lái)監(jiān)聽并完成
要想徹底理解事件代理的原理叫搁,我們需要先了解兩個(gè)概念:事件捕獲與事件冒泡
當(dāng)有下面一段頁(yè)面結(jié)構(gòu)存在,并且點(diǎn)擊了a
標(biāo)簽時(shí):
<html>
<body>
<div>
<a></a>
</div>
</body>
</html>
事件捕獲
事件的觸發(fā)順序?yàn)?html => body => div => a供炎,即一個(gè)事件將會(huì)從最不精確的對(duì)象 (html) 開始觸發(fā)常熙,一直到最精確的一個(gè)對(duì)象 (a),四個(gè)字概括就是:由外而內(nèi)
事件冒泡
與事件捕獲剛好相反碱茁,觸發(fā)順序?yàn)?a => div => body => html裸卫,由最精確的對(duì)象開始觸發(fā),一直到最不精確的對(duì)象纽竣,是 由內(nèi)而外 式的
綁定事件到 dom 元素
想要綁定事件到元素上墓贿,需要先來(lái)看下事件監(jiān)聽器的 api :
el.addEventListener(event, action, useCapture)
在這個(gè) api 中,第一個(gè)參數(shù)是觸發(fā)事件蜓氨,如點(diǎn)擊事件 ‘click’聋袋;第二個(gè)參數(shù)是事件觸發(fā)后需要執(zhí)行的方法;第三個(gè)參數(shù)的含義是是否使用事件捕獲穴吹,將該值設(shè)為true
表示在事件捕獲階段觸發(fā)
一個(gè)對(duì)象綁定一個(gè)事件
當(dāng)需要向上文頁(yè)面結(jié)構(gòu)中的a
標(biāo)簽添加一個(gè)點(diǎn)擊事件時(shí)幽勒,可以用如下代碼:
const aTag = document.querySelector('a')
aTag.addEventListener('click', e => {
console.log(e)
}) // 默認(rèn)冒泡事件機(jī)制
當(dāng)該標(biāo)簽被點(diǎn)擊時(shí)將會(huì)執(zhí)行我們定義好的回調(diào)函數(shù),這個(gè)回調(diào)函數(shù)有一個(gè)參數(shù) event(此處為了方便將參數(shù)名定義為了 e)港令,打印出來(lái)會(huì)發(fā)現(xiàn)這個(gè)參數(shù)包括了很多很多信息啥容,比如當(dāng)我們?cè)谧鐾蟿?dòng)效果的時(shí)候可能會(huì)用到的位置信息的參數(shù)等。但是其中的絕大多數(shù)參數(shù)在這次的事件代理中不會(huì)使用到
仔細(xì)查看 event 中的屬性顷霹,會(huì)發(fā)現(xiàn)有一個(gè)屬性名為target
咪惠,我們知道不管是冒泡還是捕獲,都會(huì)有一個(gè)最精確的對(duì)象和一個(gè)最不精確 的對(duì)象淋淀,而這個(gè)target
就是這個(gè)最精確的對(duì)象遥昧,也即我們實(shí)際點(diǎn)擊到的元素——a
(好奇的小伙伴也可以試著加上最后一個(gè)參數(shù)true
,將事件機(jī)制改為捕獲,回調(diào)中的參數(shù) e 依然會(huì)有target
炭臭,且指向最精確的元素)
那么如果我們將點(diǎn)擊事件綁定在a
的父級(jí)div
上再點(diǎn)擊a
永脓,此時(shí)的e.target
又會(huì)是什么呢?依然是a
鞋仍,因?yàn)槲覀儗?shí)際點(diǎn)擊的對(duì)象并沒有改變常摧,a
依然是最精確的對(duì)象。除非點(diǎn)擊在a
以外的div
區(qū)域凿试,才會(huì)使e.target
變成div
多個(gè)對(duì)象綁定一個(gè)事件
<ul>
<li></li>
<li></li>
<li></li>
<li></li>
<li></li>
</ul>
但是實(shí)際情況是排宰,我們往往需要向多個(gè)相同的節(jié)點(diǎn)中添加事件似芝,而addEventListener
這個(gè) api 只能向一個(gè)元素添加事件那婉。所以當(dāng)出現(xiàn)上面這種結(jié)構(gòu),并且需要向每一個(gè)li
添加事件時(shí)党瓮,我們可能會(huì)這樣做:
// 獲取到所有的元素详炬,返回一個(gè)包含所有對(duì)應(yīng)元素的類數(shù)組
const els = document.querySelectorAll('li')
// 遍歷每個(gè)元素并未其添加點(diǎn)擊事件
Array.prototype.forEach.call(els, el => {
el.addEventListener('click', e => {
console.log(e)
})
})
會(huì)發(fā)現(xiàn)上面這種方法是通過(guò)遍歷的方式一個(gè)一個(gè)地向元素添加所需要的事件,這種方式不得不說(shuō)是我們非常不愿意見到的
同時(shí)寞奸,我們只能夠向已經(jīng)存在的元素添加事件呛谜,如果通過(guò)異步請(qǐng)求數(shù)據(jù)后重新渲染了頁(yè)面,新增的節(jié)點(diǎn)該如何處理呢枪萄?難道再次執(zhí)行一遍相同的操作嗎隐岛?顯然,這很愚蠢
幾遍我們已經(jīng)確定不會(huì)有更多的新元素進(jìn)入頁(yè)面瓷翻,我們也不該使用這種方式達(dá)到我們的目的
實(shí)現(xiàn)一個(gè)事件代理
用過(guò) jQuery 的童鞋都知道聚凹,通過(guò)$(bigEl).on('click', el, () => {...})
的方式添加事件綁定,并且在頁(yè)面重新渲染后也不需要再次綁定齐帚,這就是事件代理的好處:一次綁定妒牙,處處通用
那么這是如何實(shí)現(xiàn)的呢?這就要用到我們上面說(shuō)到的target
屬性了对妄。原理如下:
我們先將需要執(zhí)行的事件回調(diào)綁定在一個(gè)必然存在的元素上湘今,比如body
,又比如文檔節(jié)點(diǎn)document
剪菱,當(dāng)我們指定的事件摩瞎,如click
發(fā)生時(shí),我們就能夠獲得target
屬性
由于target
指向的永遠(yuǎn)是我們實(shí)際點(diǎn)擊到的元素孝常,那么我們就可以通過(guò)這個(gè)元素來(lái)判斷是不是我們所需要被點(diǎn)擊的元素愉豺,從而判斷是否執(zhí)行回調(diào)或執(zhí)行哪一個(gè)回調(diào)
這樣,即使頁(yè)面上重新增加了元素茫因,我們也不需要對(duì)這些元素進(jìn)行再次綁定
以上面的多li
結(jié)構(gòu)為例蚪拦,代碼如下:
document.addEventListener('click', e => {
if (e.target && e.target.nodeName.toUpperCase == 'LI') {
// do something u want
alert('u clicked li element')
}
})
當(dāng)然,target
不僅能獲取到標(biāo)簽名,也能夠獲取到 class驰贷、id 和眾多屬性盛嘿,方便我們進(jìn)行更加精確的判斷
通過(guò)這種方式,我們還能實(shí)現(xiàn):一個(gè)元素綁定多個(gè)事件括袒,多個(gè)元素綁定一個(gè)事件次兆,多個(gè)事件綁定多個(gè)元素
而所有的事件事實(shí)上并不是直接與其對(duì)應(yīng)的元素關(guān)聯(lián)的,而是統(tǒng)一掛載在一個(gè)元素上锹锰,如 document / body芥炭,通過(guò)它們間接的觸發(fā)了事件,實(shí)現(xiàn)了事件代理
事件的卸載
jQuery 中提供了一個(gè)off
方法將已經(jīng)綁定的事件卸載恃慧,通過(guò)上面的學(xué)習(xí)园蝠,我們也可以實(shí)現(xiàn)這樣的事件卸載功能,只是需要繞點(diǎn)彎痢士,至于如何實(shí)現(xiàn)彪薛,我們下周再說(shuō)~~~
【結(jié)束語(yǔ):這是最近幾周以來(lái)最長(zhǎng)的一篇,希望你們喜歡怠蹂!動(dòng)動(dòng)你們的食指善延,不要吝嗇你們的喜歡哦!】