对于以下代码
<div class="爷爷">
<div class="爸爸">
<div class="儿子">文字</div>
</div>
</div>
给这三个盒子都添加事件监听
问
上述过程发生了什么,为什么点击文字,就算把三个div全部点击了呢。
DOM 事件机制主要有 2 个阶段,分别是:捕获阶段和冒泡阶段
先执行捕获阶段,再执行冒泡阶段
比如刚才说的点击事件,执行的顺序就是
只要添加了时间监听,就会按照上面的顺序执行。
事件监听函数是addEventListener('click', fn, bool)
如果不穿bool的值或者bool值为false,fn函数就只在冒泡阶段执行
如果bool的值是true,那么fn函数就在捕获阶段执行。
捕获不可以取消,但是冒泡可以取消,e.propagation()就可
但是有一些事件不可以取消冒泡,比如 scroll 事件,具体可以在 MDN 上查询
target:监听用户操作的元素
currentTarget:程序员监听的元素
<div><span>内容</span></div>
加入程序员监听的是div,用户能看见的就是内容,用户点击内容,
事件e.target就是span,而e.currentTarget就是div
先来一个应用场景:我们要给 100 个按钮添加点击事件,怎么办?最笨的办法:直接给 100 个按钮都 addEventListener有了事件委托后:监听这 100 个按钮的爸爸,等冒泡的时候,判断 target 是不是这 100 个按钮中的一个
场景二:我们要监听目前不存在的元素的点击事件,咋办?有了事件委托:监听祖先,等到冒泡时,判断点击的元素是不是我想要监听的元素
所以使用事件委托的好处就是
例子
需求:监听所有的 li 标签,如果用户点击 li 标签,就 console.log(‘用户点击了 li 标签’)
// 监听父元素 ul#test
test.addEventListener('click', (e)=> {
//通过浏览器传进来的e参数,找到当前点击元素
const t = e.target
// 判断当前元素是不是Li标签
if(t.matches('li') {
console.log('用户点击了li')
}
})
思路很简单:
利用事件委托
就实现了事件委托。
封装成函数
on("click", "#test", "li", () => {
console.log("用户点击了li");
});
function on(eventType, element, selector, fn) {
// 先判断是不是element,
//如果传进来的是选择器,不是element本身,就先变成element,
// 因为只有element才能监听事件`在这里插入代码片`
if (!(element instanceof Element)) {
element = element.querySelectorAll(element);
}
element.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 标签,
同时还必须限定,寻找的范围不能超过 element,
拿上面的例子来说,不可以越过 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);
});
}
我觉得这是一个很好的问题,很少有人思考这种问题。
我的观点是时间为委托和事件冒泡,捕获没有关系,从上面的代码可以看出,我们是获取到这个事件元素,然后再手动遍历。没有用到事件冒泡和事件捕获的特性。