事件传播模型 在说事件代理以前,先来讲一下事件模型。
在浏览器开发的早期,面对事件触发模型的问题,全部的程序员都认为事件触发不该该是直接触发的,而应该在文档中有一个传播的过程,然而事件传播的顺序应该是什么样的?
当时的程序员分为两个派别: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 属性更加保险。