mirror of
https://github.com/ZiuChen/ZiuChen.github.io.git
synced 2025-08-17 23:19:55 +08:00
186 lines
7.7 KiB
Markdown
186 lines
7.7 KiB
Markdown
# 一文读懂事件冒泡与事件捕获
|
||
|
||
## 💡 从例子入手
|
||
|
||
这是一个[简单的 Demo](https://mdn.github.io/learning-area/javascript/building-blocks/events/show-video-box.html),点击的 `Display video` 按钮后,将视频展示出来。
|
||
|
||
其中的视频 `<video>` 标签被 `<div>` 包裹,`<div>` 与 `<video>` 上都绑定了自己的 `click` 事件。
|
||
|
||
[代码片段](https://code.juejin.cn/pen/7092947625791455262)
|
||
|
||
我们的预期是:点击 `<video>` 时播放视频,点击 `<div>` 时隐藏视频,然而实际上你会发现,点击视频后,不仅视频虽然正常播放,但同时也被隐藏了。
|
||
|
||
点击子元素,父元素的事件也被触发,导致这种现象的原因正是:**浏览器的事件冒泡机制**。
|
||
|
||
## 🤔 什么是事件冒泡机制?事件捕获又是什么?
|
||
|
||
现代浏览器提供了两种事件处理阶段:**捕获阶段与冒泡阶段**,
|
||
|
||

|
||
|
||
> 在捕获阶段:
|
||
> - 浏览器检查元素的最外层祖先 `<html>` ,是否在捕获阶段中注册了一个 `onclick` 事件处理程序,如果是,则运行它。
|
||
> - 然后,它移动到 `<html>` 中单击元素的下一个祖先元素,执行相同的操作,然后是单击元素再下一个祖先元素,依此类推,直到到达实际点击的元素。
|
||
|
||
> 在冒泡阶段,与上述顺序相反:
|
||
> - 浏览器检查实际点击的元素是否在冒泡阶段中注册了一个 `onclick` 事件处理程序,如果是,则运行它
|
||
> - 然后它移动到下一个直接的祖先元素,并做同样的事情,然后是下一个,等等,直到它到达 `<html>` 元素。
|
||
|
||
当一个事件被触发时,浏览器**先运行捕获阶段,后运行冒泡阶段**,并且在默认情况下,**所有事件处理程序都在冒泡阶段进行注册**。
|
||
|
||
针对上面提到的问题,我们可以知道:当 `<video>` 点击事件触发后,虽然我们没有主动触发 `<div>` 上绑定的点击事件,但由于冒泡机制,点击事件冒泡到了 `<div>` 上,并触发了绑定在其上的监听回调函数,将 `<video>` 标签隐藏。
|
||
|
||
### 📌 用例子验证结论
|
||
|
||
下面是一个用于验证上述结论的Demo:
|
||
|
||
页面中包括由外向内的三个类名不同的div标签: `div1` `div2` `div3`,并为他们在捕获阶段/冒泡阶段分别绑定了不同的事件函数 `click` 和 `dblclick`。
|
||
|
||
[代码片段](https://code.juejin.cn/pen/7092975347720781861)
|
||
|
||
当点击最内部的 `div3` 后,浏览器控制台输出:
|
||
|
||
```
|
||
> 捕获 click div1
|
||
> 捕获 click div2
|
||
> 捕获 click div3
|
||
> 冒泡 click div3
|
||
> 冒泡 click div2
|
||
> 冒泡 click div1
|
||
```
|
||
|
||
捕获阶段先执行,由外向内,冒泡阶段后执行,由内向外。 `dblclick` 事件并未被触发。
|
||
|
||
由此可知:
|
||
|
||
* 事件触发 => 捕获阶段 => 冒泡阶段
|
||
* 默认情况下,所有事件都在冒泡阶段被注册
|
||
* 捕获阶段,浏览器**由外层向内层**逐个元素检查事件函数,如有则执行它。
|
||
* 冒泡阶段,浏览器**由内层向外层**逐个元素检查事件函数,如有则执行它。
|
||
* 子元素一个事件触发后,只有**相同的事件会被捕获/冒泡检查**
|
||
|
||
****
|
||
|
||
在本例中,通过为 `addEventListener` 函数**指定第三个参数,从而在捕获阶段监听事件**
|
||
|
||
```js
|
||
target.addEventListener(type, listener, useCapture);
|
||
```
|
||
|
||
当 `useCapture` 为 `true` 时,事件监听回调函数将在捕获阶段被触发。
|
||
|
||
## 🧐 为什么有两个阶段?它们有什么用?
|
||
|
||
### 📌 历史渊源
|
||
|
||
> 在过去,Netscape(网景)只使用事件捕获,而Internet Explorer只使用事件冒泡。当W3C决定尝试规范这些行为并达成共识时,他们最终得到了包括这两种情况(捕捉和冒泡)的系统,最终被应用在现代浏览器中。
|
||
|
||
### 📌 事件代理 (Event delegation)
|
||
|
||
利用捕获/冒泡机制,我们可以实现事件代理,什么是事件代理?
|
||
|
||
试想一下,此时有一个包含大量列表项的无序列表,我们希望每一个 `<li>` 的点击事件都能被监听并且添加特定的处理函数,然而我们不可能为每一个 `<li>` 都添加一次事件监听函数,这样效率太低了。
|
||
|
||
```js
|
||
<ul>
|
||
<li>Li.</li>
|
||
<li>Li.</li>
|
||
<li>Li.</li>
|
||
<li>Li.</li>
|
||
<li>Li.</li>
|
||
<li>Li.</li>
|
||
<li>Li.</li>
|
||
</ul>
|
||
```
|
||
|
||
这时,我们可以为最外层的 `<ul>` 绑定一个 `click` 事件的监听函数,利用捕获/冒泡机制,就可以在事件对象的 `target` 属性中拿到对应的 `<li>`。
|
||
|
||
```js
|
||
document.querySelector("ul").addEventListener("click", (e) => {
|
||
console.log(e.target); // > li (实际点击的元素)
|
||
console.log(e.currentTarget); // > ul (事件绑定的元素)
|
||
});
|
||
```
|
||
|
||
### 📌 事件对象中的`target`与`currentTarget`
|
||
|
||
在实际的使用中,你会发现事件对象中存在两个不同的属性:`target` `currentTarget`。
|
||
|
||
它们有什么区别?和回调函数中的 `this` 的关系是怎样的?
|
||
|
||
复用上面验证捕获与冒泡顺序结论的例子,下面的代码片段验证了 `target` 和 `currentTarget` 的关系。
|
||
|
||
[代码片段](https://code.juejin.cn/pen/7092980795211513892)
|
||
|
||
当点击最内部的 `div3` 后,浏览器控制台输出:
|
||
|
||
```
|
||
> 捕获 target: div3 currentTarget: div1 this: div1
|
||
> 捕获 target: div3 currentTarget: div2 this: div2
|
||
> 捕获 target: div3 currentTarget: div3 this: div3
|
||
> 冒泡 target: div3 currentTarget: div3 this: div3
|
||
> 冒泡 target: div3 currentTarget: div2 this: div2
|
||
> 冒泡 target: div3 currentTarget: div1 this: div1
|
||
```
|
||
|
||
由此可知,事件对象中的 `target` 属性为实际触发事件的DOM元素,`currentTarget` 指向注册事件监听时绑定的DOM元素。
|
||
|
||
需要注意的是,为了验证 `this` 指向,此处使用了 `function` 声明函数替代前例中的 `() => {}`,如果仍以箭头函数形式声明,则 `this` 始终指向 `Window` 对象。
|
||
|
||
## 🥳 如何阻止事件冒泡?
|
||
|
||
如你所见,大多数情况事件冒泡机制可以为我们带来便利,但是少数情况(如本文开头的例子)下,会影响预期的代码效果,我们应该如何阻止事件冒泡呢?
|
||
|
||
### 📌 .stopPropagation()
|
||
|
||
直接调用 `e.stopPropagation()` 阻止事件向上冒泡 触发其他回调
|
||
|
||
```js
|
||
document.querySelector(".div1")((e) => {
|
||
let e = e || window.event;
|
||
// some code ...
|
||
e.stopPropagation();
|
||
});
|
||
```
|
||
|
||
### 📌 e.target == e.currentTarget
|
||
|
||
当使用事件代理,给目标元素的父元素添加监听回调函数时添加判断
|
||
|
||
只有当**实际触发元素与回调绑定的元素相同**时,才触发相关逻辑
|
||
|
||
```js
|
||
document.querySelector(".div1")((e) => {
|
||
if (e.target == e.currentTarget) {
|
||
// some code ...
|
||
}
|
||
});
|
||
```
|
||
|
||
### 📌 return false
|
||
|
||
当回调内逻辑执行完毕后,直接 `return false` 可以中止事件向上冒泡
|
||
|
||
```js
|
||
document.querySelector(".div1")((e) => {
|
||
// some code ...
|
||
return false;
|
||
});
|
||
```
|
||
|
||
需要注意的是,`return false` 的方法不仅阻止了事件冒泡,而且阻止了默认事件。
|
||
|
||
> **默认事件**:DOM元素的默认行为,选中复选框是点击复选框的默认行为。下面这个例子说明了怎样阻止默认行为的发生
|
||
|
||
另一种阻止默认事件的方法是 `.preventDefault()`
|
||
|
||
```js
|
||
document.querySelector(".div1")((e) => {
|
||
// some code ...
|
||
e.preventDefault()
|
||
});
|
||
```
|
||
|
||
## 相关链接
|
||
|
||
[`事件冒泡及捕获`](https://developer.mozilla.org/zh-CN/docs/Learn/JavaScript/Building_blocks/Events#%E4%BA%8B%E4%BB%B6%E5%86%92%E6%B3%A1%E5%8F%8A%E6%8D%95%E8%8E%B7) |