前端实现捕获与冒泡
什么是捕获与冒泡
捕获与冒泡是浏览器dom流事件的概念,通俗讲就是鼠标事件如何触发的,流程是什么,比如点击某个dom元素,我们肯定能监听本次点击事件,一般来讲这次事件可以描述为,“我监听了某次点击事件,并且设置了当点击时应该发生什么,当我点击时,应该触发我预先设定好的任务”。当然,这是理所应当我们想要的情况。但是有时候情况并不像我们想的那样,比如看下面这段代码:
<style>
.box {
width: 200px;
height: 200px;
background: gainsboro;
display: flex;
align-items: center;
justify-content: center;
}
</style>
<div class="box" id="box">
<button id="btn">click me</button>
</div>
<script>
document.getElementById("box").onclick = function () {
console.log("点击了 box 元素, 应该做点什么...");
}
document.getElementById("btn").onclick = function () {
console.log("点击了 button 元素, 应该做点什么...");
}
</script>
很显然,这段代码意思是,给 div#box 添加点击事件,给 button#btn 也添加点击事件,当我们点击 button 时控制台会打印如下:
也就是说,我们点击 button 元素时同时相当于点击了 div#box,这是为什么呢?也许你会想,这个简单,div 元素里包裹着 button 元素,当我们点击 button 时其实相当于点击了 div 元素。这样想没有错,但是问题是如果我们只想当点击 button 时只触发 button 的事件,而不触发 div的事件,我们该怎么办呢?或者说当我们点击 button 时只触发 div 的事件,而不触发 button 的事件应该怎么做呢?首先我们需要明白事件是怎么发生的,请看下面这张图(盗的):
让我们来描述下上图过程,假设为点击事件: 首先鼠标点击 text 元素, 这时候浏览器需要定位到点击了哪个目标元素,它不会直接定位到那个 text 元素,而是一步步传递事件,左侧为捕获过程,也就是查找 text 元素时触发的事件,首先时 window (即窗口),然后时 document 文档,之后是body元素,然后是 div元素,然后是 text 元素,这个过程可以想象成一棵树的深度遍历过程,右侧为冒泡事件,即事件传递到目标元素以后并不会终止,而是继续向上传递,即从叶子节点到根节点。以下代码可以验证上面所述:
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<meta http-equiv="X-UA-Compatible" content="ie=edge">
<title>Document</title>
</head>
<body>
<div id="box">
<p id="text">click me!</p>
</div>
<script>
/**Dom.addEventListener 方法为监听 dom 元素的事件
* addEventListener(type, listener, useCapture)
* @ type 监听事件类型的字符串
* @ listener 一个函数或者一个实现EventListener接口的对象
* @ useCaptrue 是否使用捕获 true -> 捕获,false -> 冒泡,不填为冒泡
*/
//监听 window 点击捕获事件
window.addEventListener("click", function (event) {
console.log("点击了window,应该做点什么...", event.eventPhase);
}, true);
//监听 window 点击冒泡事件
window.addEventListener("click", function (event) {
console.log("点击了window,应该做点什么...", event.eventPhase);
}, false);
//监听 document 点击捕获事件
document.addEventListener("click", function (event) {
console.log("点击 document,应该做点什么...", event.eventPhase);
}, true);
//监听 document 点击冒泡事件
document.addEventListener("click", function (event) {
console.log("点击 document,应该做点什么...", event.eventPhase);
}, false);
//监听 body 点击捕获事件
document.body.addEventListener("click", function (event) {
console.log("点击了 body,应该做点什么...", event.eventPhase);
}, true);
//监听 body 点击冒泡事件
document.body.addEventListener("click", function (event) {
console.log("点击了 body,应该做点什么...", event.eventPhase);
}, false);
//监听 div 点击捕获事件
document.getElementById("box").addEventListener("click", function (event) {
console.log("点击了div, 应该做点什么...", event.eventPhase);
}, true);
//监听 div 点击冒泡事件
document.getElementById("box").addEventListener("click", function (event) {
console.log("点击了div, 应该做点什么...", event.eventPhase);
}, false);
//监听 p 点击捕获事件
document.getElementById("text").addEventListener("click", function (event) {
console.log("点击了p元素,应该做点什么...", event.eventPhase);
}, true);
//监听 p 点击冒泡事件
document.getElementById("text").addEventListener("click", function (event) {
console.log("点击了p元素,应该做点什么...", event.eventPhase);
}, false);
</script>
</body>
</html>
下面是点击 p 元素的浏览器打印台结果:
结合图一目了然,你可能会疑问后面的数字什么意思,在这里说明一下 event.eventPhase 这个值表示当前事件是捕获还是冒泡还是目标元素触发,1 -> 捕获,2 -> 点击目标元素,3 -> 冒泡。也就是说对于目标元素的捕获和冒泡是相同的。还记得上面的问题吗,就是点击 button 元素 只触发 button的事件,或者只触发 div的事件,也就是说我们如何阻止 捕获,冒泡事件的传递。看下面这段代码:
<div id="box">
<button id="btn">click me</button>
</div>
<script>
document.getElementById("box").addEventListener("click", function (event) {
console.log("点击了box 元素,应该做点什么...");
event.stopImmediatePropagation();
}, true);
document.getElementById("btn").addEventListener("click", function (event) {
console.log("点击了 button 元素,应该做点什么...");
}, true);
document.getElementById("btn").addEventListener("click", function (event) {
console.log("点击了 button 元素,应该做点什么...");
}, false);
</script>
在这里我们为了更好的验证,你可以看到,我们为 button 点击监听了捕获以及冒泡,当我们点击button元素时,你会看到:
当然,你没有看到 “点击了 button 元素,应该做点什么…” 字样,如果你仔细看了代码,肯定会知道是这行代码的作用:
event.stopImmediatePropagation();
没错,就是这行代码的作用,注意这个方法有一个差不多的方法:
event.stopPropagation();
显然这两个方法差 Immediate ,对于前者 会停止当前绑定的事件以及后续所有的事件,但是对于后者 会先执行完当前的事件,然后停止后面的事件。
到此你已经看到几个关键的设置都是通过 函数参数 event 的设置实现的,所以你可以查阅相关资料了解更多关于 这个函数参数的作用,推荐阅读 《javascript高级程序设计》《javascript权威指南》,你也可以在网上查阅更多相关 addEventListener 这个函数的资料。上面说到,是这行代码阻止了捕获,其实这行代码的意思是 阻止捕获以及冒泡的继续传递 上面验证了其阻止捕获,你可以自行编码验证组织冒泡。对于我们刚开始提到的问题,可用一下代码解决:
<!-- 只触发div的事件 ->
<div id="box">
<button id="btn">click me</button>
</div>
<script>
document.getElementById("box").addEventListener("click", function (event) {
console.log("点击了box 元素,应该做点什么...");
event.stopImmediatePropagation();
}, true);
document.getElementById("btn").addEventListener("click", function (event) {
console.log("点击了 button 元素,应该做点什么...");
}, true);
document.getElementById("btn").addEventListener("click", function (event) {
console.log("点击了 button 元素,应该做点什么...");
}, false);
</script>
<!-- 只触发button的事件 ->
<div id="box">
<button id="btn">click me</button>
</div>
<script>
document.getElementById("box").addEventListener("click", function (event) {
console.log("点击了box 元素,应该做点什么...");
event.stopImmediatePropagation();
}, false);
document.getElementById("btn").addEventListener("click", function (event) {
console.log("点击了 button 元素,应该做点什么...");
event.stopImmediatePropagation();
}, true);
</script>
在这里说明一下,如果你把 div 的事件绑定在捕获阶段,那你是无论如何都不可能只触发 button 的事件的,所以把 dom 元素的事件绑定在捕获还是冒泡阶段是很关键的。总之,本文只是简单介绍了一下捕获与冒泡,更多关于捕获与冒泡这些 dom 流事件的信息,请自行查阅,强烈推荐《javascript高级程序设计》这本书。