事件傳播模型 在說事件代理以前,先來講一下事件模型。
在瀏覽器開發的早期,面對事件觸發模型的問題,全部的程序員都認爲事件觸發不該該是直接觸發的,而應該在文檔中有一個傳播的過程,然而事件傳播的順序應該是什麼樣的?
當時的程序員分爲兩個派別:javascript
因而,微軟表明的事件捕獲派製做出了支持dom事件捕獲的 IE 瀏覽器。而事件冒泡派則製做出了如 Firefox 這樣支持事件冒泡的瀏覽器。
雙方意見相左,標準不一。後來,W3C橫空出世,收編兩方意見,給了一個統一的標準,就是如今的事件模型。
在 W3C 的標準中,事件捕獲和事件冒泡都是合乎規範的,開發者能夠本身指定事件的傳播模型。
那麼,什麼是事件捕獲,什麼是事件冒泡,有必要爭論嗎?css
事件捕獲:觸發一個事件時,從DOM樹的最頂層開始尋找事件監聽函數,若找到相對應事件的監聽函數,則當即執行該函數,而後繼續向下尋找, 直到尋找到觸發事件的那個元素爲止。
事件冒泡:與事件捕獲相反,事件冒泡認爲事件觸發以後,應該從觸發事件的元素往DOM樹的上層傳播,向上尋找相對應事件監聽函數,一樣是找到執行,以後繼續尋找,直到DOM樹的頂端。
因爲事件冒泡更符合人的理解,現代瀏覽器(如Chrome)默認支持事件冒泡,只有遠古時代的IE支持事件捕獲。固然,在綁定事件時,能夠指定事件傳播模型。html
關於事件模型的演示:java
<body>
<div class="red">
<div class="blue">
<div class="green">
<div class="yellow">
<div class="orange">
<div class="purple">
</div>
</div>
</div>
</div>
</div>
</div>
</body>
複製代碼
*{margin:0;padding:0;box-sizing:border-box;}
.red.active {
background: red;
}
.blue.active {
background: blue;
}
.green.active {
background: green;
}
.yellow.active {
background: yellow;
}
.orange.active {
background: orange;
}
.purple.active {
background: purple;
}
div {
border: 1px solid black;
padding: 10px;
transition: all 0.5s;
display: flex;
flex:1;
border-radius: 50%;
background: white;
}
.red{
width: 100vw;
height: 100vw;
}
複製代碼
// 捕獲模型,先捕獲,後冒泡。
let divs = $('div').get()
let n = 0
for (let i = 0; i < divs.length; i++) {
divs[i].addEventListener('click', () => {
setTimeout(() => {
divs[i].classList.add('active')
}, n * 500)
n += 1
}, true)
}
for (let i = 0; i < divs.length; i++) {
divs[i].addEventListener('click', () => {
setTimeout(() => {
divs[i].classList.remove('active')
}, n * 500)
n += 1
})
}
複製代碼
// 冒泡模型,省略捕獲,直接冒泡。
let divs = $('div').get()
let n = 0
for (let i = 0; i < divs.length; i++) {
divs[i].addEventListener('click', () => {
setTimeout(() => {
divs[i].classList.add('active')
}, n * 500)
n += 1
}, false)
}
for (let i = 0; i < divs.length; i++) {
divs[i].addEventListener('click', () => {
setTimeout(() => {
divs[i].classList.remove('active')
}, n * 500)
n += 1
})
}
複製代碼
既然事件是具備傳播性的,那麼,能不能利用這個特性搞點事情呢?程序員
事件代理的原理:利用事件模型的傳播性質,將子元素的監聽函數綁定到父元素上,經過事件傳播去執行監聽函數。瀏覽器
####場景: 假設如今有一個 ul 元素,裏面有 4 個 li 子元素,須要給4個子元素添加一個鼠標點擊事件,log 出 li 內的文本app
<ul>
<li>1</li>
<li>2</li>
<li>3</li>
<li>4</li>
</ul>
複製代碼
常規的方式是直接添加事件監聽:dom
let ul = document.querySelector('ul')
let lis = ul.querySelectorAll('ul li')
for(let i = 0; i < lis.length; i++){
lis[i].addEventListener('click',(e)=>{
console.log(e.currentTarget.textContent)
})
}
// 獲取 li 元素,遍歷全部 li 並給 li 添加事件監聽
複製代碼
在這種方法中,每個元素都添加了1個事件監聽,一共添加了4個事件監聽,內存佔用較大。函數
接下來,需求要求添加一個 li 元素,並一樣添加事件監聽。因而,這樣解決性能
let li = document.createElement('li')
li.textContent = 5
li.addEventListener('click',(e)=>{
console.log(e.currentTarget.textContent)
})
ul.appendChild(li)
// 建立一個新的 li 元素,並給該 li 元素添加事件監聽
複製代碼
目前,一共有 5 個事件監聽了,佔用內存又大了一些。
那麼,你有沒有考慮過,萬一是給 10000 個 li 元素添加監聽事件呢?那不就有 10000 個事件監聽了?萬一要新加 10000 個新元素呢?那不是要從新加 10000 個事件監聽?
怎麼解決上面說的這種問題?
使用事件代理:
let ul = document.querySelector('ul')
let lis = ul.querySelectorAll('ul li')
ul.addEventListener('click',(e)=>{
console.log(e.target.textContent)
}) // 將全部子元素的事件代理到父元素上
let li = document.createElement('li')
li.textContent = 5
ul.appendChild(li)
// 直接添加新元素,新元素的事件一樣會被代理
複製代碼
使用事件代理以後,不管有多少個子元素,都只有一個事件監聽,同時,效果也是同樣的,節約了內存。在增長新元素時,也不用再修改事件綁定。
優勢:
使用事件代理的一個問題是須要分清楚 target 和 currentTarget 兩個屬性,在適當的時候選擇適當的屬性。
一個觸發事件的對象的引用。它與event.currentTarget不一樣, 當事件處理程序在事件的冒泡或捕獲階段被調用時。————MDN
當事件遍歷DOM時,標識事件的當前目標。它老是引用事件處理程序附加到的元素,而不是event.target,它標識事件發生的元素。————MDN
MDN上對於 target 和 currentTarget 的描述有點難以理解。
實際上,target 就是觸發事件的元素自己,不必定是綁定事件監聽的元素。而 currentTarget 則必定是綁定事件監聽的元素,不必定是觸發事件的元素。 代碼演示:
<ul>dsfasdf
<li>1</li>
<li>2</li>
<li>3</li>
<li>4</li>
</ul>
複製代碼
let ul = document.querySelector('ul')
ul.addEventListener('click',(e)=>{
console.log('我打印出的是target的值:' + e.target.textContent)
console.log(e.target.textContent)
console.log('我打印出的是currentTarget的值')
console.log(e.currentTarget.textContent)
if(e.target === e.currentTarget){
console.log(1)
}
},false)
// 點擊第4個li
// 控制檯將會打印出:
// 我打印出的是target的值:4
// 我打印出的是currentTarget的值:dsfasdf
// 1
// 2
// 3
// 4
複製代碼
看起來控制檯打印出了 6 行,那麼是否是因此的 li 都會被冒泡到呢?其實不是。
實際上,控制檯只打印了 2 行,1 行是點擊的 4,另外一行是整個 ul ,因此全部元素都被打印出來了。
當綁定事件監聽的元素和觸發事件的元素是同一個時,target === currentTarget。
在上面的例子中,就是點擊 ul 時,target 纔等於 currentTarget。
因此使用事件代理,必須使用 target,不能使用 currentTarget。
當一個事件處理函數綁定到多個元素上時,因爲冒泡和捕獲機制的存在,使用target可能會錯誤觸發不想觸發的元素,因此使用 currentTarget 屬性更加保險。