「0」引入
<div class="爷爷">
<div class="爸爸">
<div class="儿子">
文字
</div>
</div>
</div>
给爷爷div, 爸爸div, 儿子div 分别添加事件监听fn1, fn2, fn3
提问1:
点击文字,算不算点击儿子?
点击文字,算不算点击爸爸?
点击文字,算不算点击爷爷?
答案:都算
提问2:
点击文字,最先调用fn1, fn2, fn3的哪一个?
答案是都行,W3C发布了标准,支持两种调用顺序。
如果是捕获机制,就先从外到内,先调用fn1
如果是冒泡机制,就从内到外,即先调用fn3
接下来我们详细分析DOM事件机制。DOM事件机制主要有2个阶段,分别是:捕获阶段和冒泡阶段
「一」DOM事件机制
当一个事件发生在具有父元素的元素上时,现代浏览器运行两个不同的阶段 - 捕获阶段和冒泡阶段。
捕获阶段
- 浏览器检查元素的最外层祖先,是否在捕获阶段中注册了一个onclick事件处理程序,如果是,则运行它。
- 然后,它移动到中单击元素的下一个祖先元素,并执行相同的操作,然后是单击元素再下一个祖先元素,依此类推,直到到达实际点击的元素。
冒泡阶段
- 浏览器首先检查被点击元素,(在开头的例子中,就是文字),然后看是否在冒泡阶段中有Onclick事件,如果是,就运行
- 然后,寻找下一个parentNode, (在开头的例子中,就是儿子div),然后看是否在冒泡阶段中有Onclick事件,如果是,就运行
- 然后再找下一个ParentNode, (在开头的例子中,就是爸爸div)
如图所示, 我们在使用addEventListener监听事件时,addEventListener('click', fn, bool)
- 如果第三个参数bool 不传,或者传falsy值, 那么我们会在冒泡阶段调用fn
- 如果第三个参数bool 传值为true, 那么我们会在捕获阶段调用fn 每次点击之后都会按先捕获再冒泡的顺序走一遍,有函数就执行,没有函数就跳过
一个特例
如果 只有一个div被监听(不考虑父子同时被监听);
fn同时在捕获阶段和冒泡阶段监听click事件;
用户点击的元素就是开发者监听的,
那么谁先监听谁先执行
取消冒泡
捕获不可以取消,但是冒泡可以取消,e.stopPropagation()可中断冒泡,浏览器不再向上走。
但是有一些事件不可取消冒泡,比如scroll事件(具体可以在MDN上查询)
那么如何防止滚动?
- 可阻止wheel 和 touchstart的默认动作;
- 再让css取消滚动条
target vs currentTarget
e.target 用户正在操作的元素
e.currentTarget 程序员在监听的元素
<div>
<span>文字</span>
</div>
假设我们监听的是div, 但用户实际点击的是文字,那么
e.target就是span
e.currentTarget就是div
「二」事件委托
由于冒泡阶段,浏览器从用户点击的内容从下往上遍历至 window,逐个触发事件处理函数,因此可以监听一个祖先节点(例如爸爸节点、爷爷节点)来同时处理多个子节点的事件。
简单理解:委托一个元素帮我监听我本该监听的事件
应用场景
场景一
我们要给100个按钮添加点击事件,怎么办?
最笨的办法:直接给100个按钮都addEventListener
有了事件委托后:监听这100个按钮的爸爸,等冒泡的时候,判断target是不是这100个按钮中的一个
代码实现
监听所有的button标签,如果用户点击button标签,就console.log('button 被点击了')
<div id=div1>
<span>span1</span>
<button>click 1</button>
<button>click 2</button>
<button>click 3</button>
</div>
div1.addEventListener('click', (e)=>{
const t = e.target // t 当作 被用户操作的元素
if(t.tagName.toLowerCase() === 'button'){
console.log('button 被点击了')
console.log('button 内容是 ' + t.textContent)
}
})
场景二
我们要监听目前不存在的元素的点击事件,则么办?
有了事件委托:监听祖先,等到冒泡时,判断点击的元素是不是我想要监听的元素
代码实现
<div id=div1></div>
setTimeout(()=>{
const button = document.createElement('button')
button.textContent = 'click 1'
div1.appendChild(button)
}, 1000)
div1.addEventListener('click', (e)=>{
const t = e.target
if(t.tagName.toLowerCase() === 'button'){
console.log('button被点击了')
}
})
事件委托的优点
- 省监听数量(省内存)
- 可以监听动态元素