DOM事件流(event flow )存在三个阶段:事件捕获阶段、处于目标阶段、事件冒泡阶段。
Dom标准事件流的触发的先后顺序为:先捕获再冒泡。即当触发dom事件时,会先进行事件捕获,捕获到事件源之后通过事件传播进行事件冒泡。
addEventListener的第三个参数
在我们平常用的addEventListener方法中,一般只会用到两个参数,一个是需要绑定的事件,另一个是触发事件后要执行的函数,然而addEventListener还可以传入第三个参数:
element.addEventListener(event, function, useCapture);
第三个参数默认值是false,表示在事件冒泡阶段调用事件处理函数;如果参数为true,则表示在事件捕获阶段调用处理函数。如果不写第三个参数则默认在事件冒泡阶段调用事件处理函数。
下面先介绍事件冒泡:
事件冒泡
事件冒泡(dubbed bubbling):当一个元素接收到事件的时候,会把他接收到的事件传给自己的父级,一直到 window (注意这里传递的仅仅是事件,例如click、focus等等这些事件, 并不传递所绑定的事件函数。)
事件源 =>根节点(由内到外)进行事件传播。
举例说明:
给三个盒子依次绑定点击事件,当点击盒子的时候,会依次触发父级元素的点击事件。
let big = document.querySelector('.big')
let center = document.querySelector('.center')
let small = document.querySelector('.small')
big.addEventListener('click', () => {
console.log('big')
})
center.addEventListener('click', () => {
console.log('center')
})
small.addEventListener('click',()=>{
console.log('small')
})
输出如下
如果父元素没有绑定点击事件则只会触发点击盒子的事件。
let big = document.querySelector('.big')
let center = document.querySelector('.center')
let small = document.querySelector('.small')
// big.addEventListener('click', () => {
// console.log('big')
// })
// center.addEventListener('click', () => {
// console.log('center')
// })
small.addEventListener('click', () => {
console.log('small')
})
如果子元素(small)的点击事件去掉,当我们点击small的时候会把当前操作的点击事件传递给父元素(因为父元素绑定了点击函数)
let big = document.querySelector('.big')
let center = document.querySelector('.center')
let small = document.querySelector('.small')
big.addEventListener('click', () => {
console.log('big')
})
center.addEventListener('click', () => {
console.log('center')
})
// small.addEventListener('click', () => {
// console.log('small')
// })
有些时候我们不希望产生事件冒泡,所以可以 在子事件中加入e.stopPropagation() 取消冒泡
small.addEventListener('click', e => {
e.stopPropagation() // 阻止冒泡
console.log('small')
})
这是时候再次点就,其他绑定了点击事件的父元素就不会再响应了
事件捕获
事件捕获(event capturing): 当鼠标点击或者触发dom事件时(被触发dom事件的这个元素被叫作事件源),浏览器会从根节点 =>事件源(由外到内)进行事件传播。
事件捕获与事件冒泡是比较类似的,最大的不同在于事件传播的方向。
还是举上面的例子:
tips; 因为事件冒泡是默认的,所以不做特殊处理
let big = document.querySelector('.big')
let center = document.querySelector('.center')
let small = document.querySelector('.small')
big.addEventListener('click', () => {
console.log('big-----事件捕获')
}, true)
center.addEventListener('click', () => {
console.log('center-----事件捕获')
}, true)
small.addEventListener('click', e => {
// e.stopPropagation() // 阻止冒泡
console.log('small-----事件捕获')
}, true)
big.addEventListener('click', () => {
console.log('big-----事件冒泡')
})
center.addEventListener('click', () => {
console.log('center-----事件冒泡')
})
small.addEventListener('click', e => {
// e.stopPropagation() // 阻止冒泡
console.log('small-----事件冒泡')
},)
输出如下
事件委托
事件委托也称为事件代理。就是利用事件冒泡,把子元素的事件都绑定到父元素上。如果子元素阻止了事件冒泡,那么委托就无法实现。
不是每个子节点单独设置事件监听器,而是事件监听器设置在其父节点上,然后利用冒泡原理影响设置每个子节点。
应用场景:1000个button需要注册点击事件
如果循环给每个按钮添加点击事件,那么会增加内存损耗,影响性能
let btns = document.getElementsByTagName('button');
console.log(btns);
for (let i = 0; i < btns.length; i++) {
btns[i].addEventListener('click', function () {
console.log(i);
})
}
此时可以给button的父元素添加点击事件
let btnFather = document.getElementById('btnfather');
btnFather.addEventListener('click', function () {
console.log(e.target.innerHTML)
}
这时相当于每个按钮都绑定了点击事件
优点:
-
替代循环绑定事件的操作,减少内存消耗,提高性能。比如:
在table上代理所有td的click事件。 在ul上代理所有li的click事件。
-
简化了dom节点更新时,相应事件的更新。比如:
不用在新添加的li上绑定click事件。 当删除某个li时,不用移解绑上面的click事件。
缺点:
- 事件委托基于冒泡,对于不冒泡的事件不支持。
- 层级过多,冒泡过程中,可能会被某层阻止掉。
- 理论上委托会导致浏览器频繁调用处理函数,虽然很可能不需要处理。所以建议就近委托,比如在table上代理td,而不是在document上代理td。