1 引入
对于下列代码:
<div class="爷爷">
<div class="爸爸">
<div class="儿子">
文字
</div>
</div>
</div>
复制代码
同时我们爷爷div, 爸爸div, 儿子div 分别添加事件监听fn1, fn2, fn3
提问1:
- 点击文字,算不算点击儿子?
- 点击文字,算不算点击爸爸?
- 点击文字,算不算点击爷爷?
答案:都算
提问2:
- 点击文字,最先调用fn1, fn2, fn3的哪一个?
答案:都行,如果是捕获机制,就先从外到内,先调用fn1
如果是冒泡机制,就从内到外,即先调用fn3
接下来我们详细分析DOM事件机制和事件委托
2 DOM 事件机制
DOM事件机制主要有2个阶段,分别是:捕获阶段和冒泡阶段
2.1 什么是捕获和冒泡?
当一个事件发生在具有父元素的元素上时,现代浏览器运行两个不同的阶段 – 捕获阶段和冒泡阶段。
在捕获阶段:
- 浏览器检查元素的最外层祖先
<html>
,是否在捕获阶段中注册了一个onclick
事件处理程序,如果是,则运行它。 - 然后,它移动到
<html>
中单击元素的下一个祖先元素,并执行相同的操作,然后是单击元素再下一个祖先元素,依此类推,直到到达实际点击的元素。
在冒泡阶段:
- 浏览器首先检查被点击元素,(在开头的例子中,就是文字),然后看是否在冒泡阶段中有
Onclick
事件,如果是,就运行 - 然后,寻找下一个
parentNode
, (在开头的例子中,就是儿子div) - 然后看是否在冒泡阶段中有
Onclick
事件,如果是,就运行
我们在使用 addEventListener 监听事件时,addEventListener(‘click’, fn, bool)
如果第三个参数 bool 不传,或者传 false, 那么我们会在冒泡阶段调用 fn
如果第三个参数 Bool 传值为 true, 那么我们会在捕获阶段调用 fn
2.2 取消冒泡
捕获不可以取消,但是冒泡可以取消,e.propagation()
就可
但是有一些事件不可以取消冒泡,比如scroll事件
,具体可以在MDN上查询
2.3 target 和 currentTarget的区别
e.target 用户正在操作的元素
e.currentTarget 程序员在监听的元素
举个例子:
<div>
<span>文字</span>
</div>
复制代码
假设我们监听的是div, 但用户实际点击的是文字,那么
e.target
就是span标签
e.currentTarget
就是div标签
2.4 总结捕获和冒泡:
捕获:当用户点击按钮,浏览器会从 window 从上向下遍历至用户点击的按钮,逐个触发事件处理函数。
冒泡:浏览器从用户点击的按钮从下往上遍历至 window,逐个触发事件处理函数。
3 事件委托
3.1 什么是事件委托
由于冒泡阶段,浏览器从用户点击的内容从下往上遍历至 window,逐个触发事件处理函数,
因此可以监听一个祖先节点(例如爸爸节点、爷爷节点)来同时处理多个子节点的事件
3.2 常见应用场景
场景一:
我们要给100个按钮添加点击事件,怎么办?
最笨的办法:直接给100个按钮都addEventListener
有了事件委托后:监听这100个按钮的爸爸,等冒泡的时候,判断target是不是这100个按钮中的一个
场景二:
我们要监听目前不存在的元素的点击事件,咋办?
有了事件委托:监听祖先,等到冒泡时,判断点击的元素是不是我想要监听的元素
所以使用事件委托的好处
就是
可以省掉监听数,从而节省内存
3.3 代码实现
需求:监听所有的li标签,如果用户点击li标签,就console.log(‘用户点击了Li标签’)
<ul id="test">
<li>1</li>
<li>2</li>
<li>3</li>
<li>4</li>
</ul>
复制代码
实现的JS代码
// 监听父元素 ul#test
test.addEventListener('click', (e)=> {
//通过浏览器传进来的e参数,找到当前点击元素
const t = e.target
// 判断当前元素是不是Li标签
if(t.matches('li') {
console.log('用户点击了li')
}
})
复制代码
实现思路很简单
- 首先监听父元素,
- 然后根据浏览器传进去的事件信息,拿到当前点击元素,
- 再判断当前点击元素是不是li元素, 如果是,就console.log(‘用户点击Li标签’)
基于此,我们可以封装一个事件委托函数
on('click', '#test', 'li', ()=>{
console.log('用户点击了li')
})
function on(eventType, parentElement, selector, fn) {
// 先判断是不是element,
//如果传进来的是选择器,不是element本身,就先变成element,
// 因为只有element才能监听事件
if (!(parentElement instanceof Element)) {
parentElement = parentElement.querySelectorAll(parentElement)
}
parentElement.addEventListener(eventType, (e)=>{
let target = e.target
if (target.matches(selector)) {
fn(e)
}
})
}
复制代码
但是以上这种实现有一个小问题,那就是如果被点击元素有多个父元素怎么办?
<ul id="test">
<li>
<p>
<span>1</span>
</p>
</li>
<li>
<p>
<span>2</span>
</p>
</li>
<li>
<p>
<span>3</span>
</p>
</li>
<li>
<p>
<span>4</span>
</p>
</li>
</ul>
复制代码
我们需要做的就是:
递归地向上多找几层父节点,直到找到li标签,
同时还必须限定,寻找的范围不能超过parentElement,
拿上面的例子来说,不可以越过ul标签,去找body标签
on('click', '#test', 'li', ()=>{
console.log('用户点击了li')
})
function on(eventType, element, selector, fn) {
if (!(element instanceof Element)) {
element = document.querySelectorAll(element)
}
element.addEventListener(eventType, (e)=>{
let target = e.target
// 如果匹配到了selector就跳出循环
while(!target.matches(selector)){
if (target === element){
//已经找到了父元素,说明还没找到,就设置为null
target = null
break
}
target = target.parentNode
}
// 找到了target, 就调用函数
target && fn.call(target, e)
})
}
复制代码
4 总结
4.1 总结捕获和冒泡
- 捕获:当用户点击按钮,浏览器会从 window 从上向下遍历至用户点击的按钮,逐个触发事件处理函数。
- 冒泡:浏览器从用户点击的按钮从下往上遍历至 window,逐个触发事件处理函数。
4.2 什么是事件委托
监听祖先元素,从而监听一个,同时操作多个后代
- 由于冒泡阶段,浏览器从用户点击的内容从下往上遍历至 window,逐个触发事件处理函数,
- 因此可以监听一个祖先节点(例如爸爸节点、爷爷节点)来同时处理多个子节点的事件