
當(dāng)我們需要監(jiān)聽某個元素被點擊的時候,我們會給這個元素添加事件監(jiān)聽器
事件監(jiān)聽器只會綁定到當(dāng)前 DOM 中已有的元素上,而實際需求中,往往會有臨時渲染出來的元素也需要監(jiān)聽事件?那這時候改怎么辦呢?難道在每次頁面渲染結(jié)束后,都再綁定一次事件監(jiān)聽嗎?
什么是 事件代理
從字面上來理解,“代理”即將自己要做的事交給別人來做。那么這邊的“事件代理”又是什么呢?同樣的,如果原本有某個事件 A 是元素 a 的事件,但是 A 事件并不直接由 a 來完成,而是轉(zhuǎn)交給元素 b 來監(jiān)聽并完成
要想徹底理解事件代理的原理,我們需要先了解兩個概念:事件捕獲與事件冒泡
當(dāng)有下面一段頁面結(jié)構(gòu)存在,并且點擊了a標(biāo)簽時:
<html>
<body>
<div>
<a></a>
</div>
</body>
</html>
事件捕獲
事件的觸發(fā)順序為 html => body => div => a,即一個事件將會從最不精確的對象 (html) 開始觸發(fā),一直到最精確的一個對象 (a),四個字概括就是:由外而內(nèi)
事件冒泡
與事件捕獲剛好相反,觸發(fā)順序為 a => div => body => html,由最精確的對象開始觸發(fā),一直到最不精確的對象,是 由內(nèi)而外 式的
綁定事件到 dom 元素
想要綁定事件到元素上,需要先來看下事件監(jiān)聽器的 api :
el.addEventListener(event, action, useCapture)
在這個 api 中,第一個參數(shù)是觸發(fā)事件,如點擊事件 ‘click’;第二個參數(shù)是事件觸發(fā)后需要執(zhí)行的方法;第三個參數(shù)的含義是是否使用事件捕獲,將該值設(shè)為true表示在事件捕獲階段觸發(fā)
一個對象綁定一個事件
當(dāng)需要向上文頁面結(jié)構(gòu)中的a標(biāo)簽添加一個點擊事件時,可以用如下代碼:
const aTag = document.querySelector('a')
aTag.addEventListener('click', e => {
console.log(e)
}) // 默認(rèn)冒泡事件機(jī)制
當(dāng)該標(biāo)簽被點擊時將會執(zhí)行我們定義好的回調(diào)函數(shù),這個回調(diào)函數(shù)有一個參數(shù) event(此處為了方便將參數(shù)名定義為了 e),打印出來會發(fā)現(xiàn)這個參數(shù)包括了很多很多信息,比如當(dāng)我們在做拖動效果的時候可能會用到的位置信息的參數(shù)等。但是其中的絕大多數(shù)參數(shù)在這次的事件代理中不會使用到
仔細(xì)查看 event 中的屬性,會發(fā)現(xiàn)有一個屬性名為target,我們知道不管是冒泡還是捕獲,都會有一個最精確的對象和一個最不精確 的對象,而這個target就是這個最精確的對象,也即我們實際點擊到的元素——a
(好奇的小伙伴也可以試著加上最后一個參數(shù)true,將事件機(jī)制改為捕獲,回調(diào)中的參數(shù) e 依然會有target,且指向最精確的元素)
那么如果我們將點擊事件綁定在a的父級div上再點擊a,此時的e.target又會是什么呢?依然是a,因為我們實際點擊的對象并沒有改變,a依然是最精確的對象。除非點擊在a以外的div區(qū)域,才會使e.target變成div
多個對象綁定一個事件
<ul>
<li></li>
<li></li>
<li></li>
<li></li>
<li></li>
</ul>
但是實際情況是,我們往往需要向多個相同的節(jié)點中添加事件,而addEventListener這個 api 只能向一個元素添加事件。所以當(dāng)出現(xiàn)上面這種結(jié)構(gòu),并且需要向每一個li添加事件時,我們可能會這樣做:
// 獲取到所有的元素,返回一個包含所有對應(yīng)元素的類數(shù)組
const els = document.querySelectorAll('li')
// 遍歷每個元素并未其添加點擊事件
Array.prototype.forEach.call(els, el => {
el.addEventListener('click', e => {
console.log(e)
})
})
會發(fā)現(xiàn)上面這種方法是通過遍歷的方式一個一個地向元素添加所需要的事件,這種方式不得不說是我們非常不愿意見到的
同時,我們只能夠向已經(jīng)存在的元素添加事件,如果通過異步請求數(shù)據(jù)后重新渲染了頁面,新增的節(jié)點該如何處理呢?難道再次執(zhí)行一遍相同的操作嗎?顯然,這很愚蠢
幾遍我們已經(jīng)確定不會有更多的新元素進(jìn)入頁面,我們也不該使用這種方式達(dá)到我們的目的
實現(xiàn)一個事件代理
用過 jQuery 的童鞋都知道,通過$(bigEl).on('click', el, () => {...})的方式添加事件綁定,并且在頁面重新渲染后也不需要再次綁定,這就是事件代理的好處:一次綁定,處處通用
那么這是如何實現(xiàn)的呢?這就要用到我們上面說到的target屬性了。原理如下:
我們先將需要執(zhí)行的事件回調(diào)綁定在一個必然存在的元素上,比如body,又比如文檔節(jié)點document,當(dāng)我們指定的事件,如click發(fā)生時,我們就能夠獲得target屬性
由于target指向的永遠(yuǎn)是我們實際點擊到的元素,那么我們就可以通過這個元素來判斷是不是我們所需要被點擊的元素,從而判斷是否執(zhí)行回調(diào)或執(zhí)行哪一個回調(diào)
這樣,即使頁面上重新增加了元素,我們也不需要對這些元素進(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)行更加精確的判斷
通過這種方式,我們還能實現(xiàn):一個元素綁定多個事件,多個元素綁定一個事件,多個事件綁定多個元素
而所有的事件事實上并不是直接與其對應(yīng)的元素關(guān)聯(lián)的,而是統(tǒng)一掛載在一個元素上,如 document / body,通過它們間接的觸發(fā)了事件,實現(xiàn)了事件代理
事件的卸載
jQuery 中提供了一個off方法將已經(jīng)綁定的事件卸載,通過上面的學(xué)習(xí),我們也可以實現(xiàn)這樣的事件卸載功能,只是需要繞點彎,至于如何實現(xiàn),我們下周再說~~~
【結(jié)束語:這是最近幾周以來最長的一篇,希望你們喜歡!動動你們的食指,不要吝嗇你們的喜歡哦!】
