2021.12.27
当你对一个 DOM 元素发起一个 action 的时候,比如点击,那么浏览器会向这个元素发送一个事件,而你可以给这个元素添加 event listener 来监听这个事件。
在 HTML 里所有 DOM 元素空间上都是重叠的,比如你点击了一个元素,那么理论上这个事件会派发给所有和你点击的这个元素重叠的元素,所以就有了 3 个 事件派发阶段,捕获(caputring),目标(target),冒泡(bubbling)。
事件身上有一个 path 的非标准的属性(标准的话你可以通过 event 身上的 composedPath() 方法来获取 path)可以用来查看所有浏览器会派发事件的对象,

最左边的是嵌套最深的被你点击的元素,最右边是被你点击的元素的最顶层的父级元素,整个 path 包含了所有的重叠元素。
浏览器事件派发阶段的顺序是,捕获 -> 目标 -> 冒泡。
也就是说,从 path 的右边向左开始派发事件,也就是捕获阶段,你可以通过 addEventListener('someEvent', myEventHandler, true 给元素添加捕获阶段的 listener,等到了最左边的那个元素,就是目标阶段,然后从 path 的最左边的那个元素的右边开始向右开始派发事件,也就是冒泡阶段。
我们可以利用捕获或者冒泡来实现event delegation,一般使用冒泡来实现,因为它的兼容性好一点。
这样事件基本的要点就讲完了,总结下,
基本概念讲完了,我们再来补充下一些其他点,你知道了浏览器会按照 path 来派发事件,那么我们如何中断浏览器继续派发事件呢?
事件对象身上的 stopPropagation 和 stopImmediatePropagation 方法可以帮你中断事件派发。
The stopPropagation() method of the Event interface prevents further propagation of the current event in the capturing and bubbling phases.
Demo 源码在这
stopPropagation 会阻止浏览器往 path 上的下一个元素进行事件派发。
<script>
document.body.addEventListener("click", () => {
console.log("body clicked");
});
const out = document.getElementById("out");
out.addEventListener("click", (e) => {
console.log("out click1", e, e.composedPath());
e.stopPropagation();
});
out.addEventListener("click", (e) => {
console.log("out click2", e, e.composedPath());
});
const inner2 = document.getElementById("inner2");
inner2.addEventListener("click", (e) => {
console.log("inner2", e, e.composedPath());
});
</script>

你可以看到 body 身上的 listener 没有被调用,也就是 out 这个元素成功阻止了浏览器的事件派发。
还有一个现象,就是调用了 e.stopPropagation 方法的元素,浏览器往该元素身上派发的事件,它身上的所有监听方法都被调用了。如果我们想让调用了 e.stopPropagation 之后,不仅事件不会继续往下一个元素上派发,而且该元素身上的其他 listener 也不会继续被调用的话,我们可以使用 stopImmediatePropagation 方法。
Demo 源码在这
The stopImmediatePropagation() method of the Event interface prevents other listeners of the same event from being called.
<div id="out">
<div id="inner1">
<div id="inner2">inner2</div>
</div>
</div>
<script>
document.body.addEventListener("click", () => {
console.log("body clicked");
});
const out = document.getElementById("out");
out.addEventListener("click", (e) => {
console.log("out click1", e, e.composedPath());
e.stopImmediatePropagation();
});
out.addEventListener("click", (e) => {
console.log("out click2", e, e.composedPath());
});
const inner2 = document.getElementById("inner2");
inner2.addEventListener("click", (e) => {
console.log("inner2", e, e.composedPath());
});
</script>

所以当 out click1 被调用的时候,该元素身上的另一个 event-handler out click2 就不会被调用了,并且会阻止冒泡/捕获,所以 body 身上的 click event-handler 也不会被调用了。
其他,事件对象身上还有一个常用的最后的一个方法,preventDefault,他可以帮助你组织浏览器的默认行为。
源码在这
The preventDefault() method of the Event interface tells the user agent that if the event does not get explicitly handled, its default action should not be taken as it normally would be.
“Toggling a checkbox is the default action of clicking on a checkbox”, 所以使用了 preventDefault 就会阻止 toggling。
它并不会阻止冒泡或者捕获,只是阻止了默认行为。
<script>
document.body.addEventListener("click", () => {
console.log("body clicked");
});
const out = document.getElementById("out");
out.addEventListener("click", (e) => {
console.log("out click1", e, e.composedPath());
e.preventDefault();
});
out.addEventListener("click", (e) => {
console.log("out click2", e, e.composedPath());
});
const inner2 = document.getElementById("inner2");
inner2.addEventListener("click", (e) => {
console.log("inner2", e, e.composedPath());
});
</script>

再举个例子,浏览器默认的行为是,页面上的元素不是 drop 区域,如果你想取消这个行为,就可以利用 preventDefault 来取消这个行为来让某个元素变成可被 drop 的区域,详情请见这里。
点击这个 absolute positioned 的元素的话,在它下面的 relative positioned 元素是不会被派发事件的,path 也就是由 absolute positioned 元素的 parents 构成的 path,为了解决这种覆盖的问题,所以也就有了 pointer-event 那么一个 CSS 属性。
addEventListener 往元素身上添加的 listeners 是一个 Set,不是 Array,可以看下这里。