一、引入:
对于以下代码
<div class="爷爷">
<div class="爸爸">
<div class="儿子">文字</div>
</div>
</div>
同时我们爷爷 div, 爸爸 div, 儿子 div 分别添加事件监听 fn1, fn2, fn3
提问 1:
- 点击文字,算不算点击儿子?
- 点击文字,算不算点击爸爸?
- 点击文字,算不算点击爷爷?
答案:都算
提问 2:
- 点击文字,最先调用 fn1, fn2, fn3 的哪一个?
- 答案是都行,如果是捕获机制,就先从外到内,先调用 fn1
- 如果是冒泡机制,就从内到外,即先调用 fn3
二、DOM 事件机制
DOM 事件机制主要有 2 个阶段,分别是:捕获阶段和冒泡阶段
2.1 什么是捕获和冒泡?
当一个事件发生在具有父元素的元素上时,现代浏览器运行两个不同的阶段 - 捕获阶段和冒泡阶段。
在捕获阶段:
- 浏览器检查元素的最外层祖先
<html>,是否在捕获阶段中注册了一个onclick事件处理程序,如果是,则运行它。 - 然后,它移动到
<html>中单击元素的下一个祖先元素,并执行相同的操作,然后是单击元素再下一个祖先元素,依此类推,直到到达实际点击的元素。
在冒泡阶段:
- 浏览器首先检查被点击元素,(在开头的例子中,就是文字),然后看是否在冒泡阶段中有 Onclick 事件,如果是,就运行
- 然后,寻找下一个 parentNode, (在开头的例子中,就是儿子 div),然后看是否在冒泡阶段中有 Onclick 事件,如果是,就运行
- 然后再找下一个 ParentNode, (在开头的例子中,就是爸爸 div)
我们在使用 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.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.1 总结捕获和冒泡:
捕获:当用户点击按钮,浏览器会从 window 从上向下遍历至用户点击的按钮,逐个触发事件处理函数。
冒泡:浏览器从用户点击的按钮从下往上遍历至 window,逐个触发事件处理函数。
4.2 什么是事件委托
【监听祖先元素,从而监听一个,同时操作多个后代】
由于冒泡阶段,浏览器从用户点击的内容从下往上遍历至 window,逐个触发事件处理函数,
因此可以监听一个祖先节点(例如爸爸节点、爷爷节点)来同时处理多个子节点的事件