import{_ as s,o as n,c as a,a as e}from"./app.a81d7d4f.js";const u=JSON.parse('{"title":"一文读懂事件冒泡与事件捕获","description":"","frontmatter":{},"headers":[{"level":2,"title":"💡 从例子入手","slug":"💡-从例子入手","link":"#💡-从例子入手","children":[]},{"level":2,"title":"🤔 什么是事件冒泡机制?事件捕获又是什么?","slug":"🤔-什么是事件冒泡机制-事件捕获又是什么","link":"#🤔-什么是事件冒泡机制-事件捕获又是什么","children":[{"level":3,"title":"📌 用例子验证结论","slug":"📌-用例子验证结论","link":"#📌-用例子验证结论","children":[]}]},{"level":2,"title":"🧐 为什么有两个阶段?它们有什么用?","slug":"🧐-为什么有两个阶段-它们有什么用","link":"#🧐-为什么有两个阶段-它们有什么用","children":[{"level":3,"title":"📌 历史渊源","slug":"📌-历史渊源","link":"#📌-历史渊源","children":[]},{"level":3,"title":"📌 事件代理 (Event delegation)","slug":"📌-事件代理-event-delegation","link":"#📌-事件代理-event-delegation","children":[]},{"level":3,"title":"📌 事件对象中的target与currentTarget","slug":"📌-事件对象中的target与currenttarget","link":"#📌-事件对象中的target与currenttarget","children":[]}]},{"level":2,"title":"🥳 如何阻止事件冒泡?","slug":"🥳-如何阻止事件冒泡","link":"#🥳-如何阻止事件冒泡","children":[{"level":3,"title":"📌 .stopPropagation()","slug":"📌-stoppropagation","link":"#📌-stoppropagation","children":[]},{"level":3,"title":"📌 e.target == e.currentTarget","slug":"📌-e-target-e-currenttarget","link":"#📌-e-target-e-currenttarget","children":[]},{"level":3,"title":"📌 return false","slug":"📌-return-false","link":"#📌-return-false","children":[]}]},{"level":2,"title":"相关链接","slug":"相关链接","link":"#相关链接","children":[]}],"relativePath":"article/一文读懂事件冒泡与事件捕获.md","lastUpdated":1675779808000}'),l={name:"article/一文读懂事件冒泡与事件捕获.md"},p=e(`

一文读懂事件冒泡与事件捕获

💡 从例子入手

这是一个简单的 Demo,点击的 Display video 按钮后,将视频展示出来。

其中的视频 <video> 标签被 <div> 包裹,<div><video> 上都绑定了自己的 click 事件。

代码片段

我们的预期是:点击 <video> 时播放视频,点击 <div> 时隐藏视频,然而实际上你会发现,点击视频后,不仅视频虽然正常播放,但同时也被隐藏了。

点击子元素,父元素的事件也被触发,导致这种现象的原因正是:浏览器的事件冒泡机制

🤔 什么是事件冒泡机制?事件捕获又是什么?

现代浏览器提供了两种事件处理阶段:捕获阶段与冒泡阶段

bubbling-capturing.png

在捕获阶段:

在冒泡阶段,与上述顺序相反:

当一个事件被触发时,浏览器先运行捕获阶段,后运行冒泡阶段,并且在默认情况下,所有事件处理程序都在冒泡阶段进行注册

针对上面提到的问题,我们可以知道:当 <video> 点击事件触发后,虽然我们没有主动触发 <div> 上绑定的点击事件,但由于冒泡机制,点击事件冒泡到了 <div> 上,并触发了绑定在其上的监听回调函数,将 <video> 标签隐藏。

📌 用例子验证结论

下面是一个用于验证上述结论的Demo:

页面中包括由外向内的三个类名不同的div标签: div1 div2 div3,并为他们在捕获阶段/冒泡阶段分别绑定了不同的事件函数 clickdblclick

代码片段

当点击最内部的 div3 后,浏览器控制台输出:

> 捕获 click div1
> 捕获 click div2
> 捕获 click div3
> 冒泡 click div3
> 冒泡 click div2
> 冒泡 click div1

捕获阶段先执行,由外向内,冒泡阶段后执行,由内向外。 dblclick 事件并未被触发。

由此可知:


在本例中,通过为 addEventListener 函数指定第三个参数,从而在捕获阶段监听事件

js
target.addEventListener(type, listener, useCapture);

useCapturetrue 时,事件监听回调函数将在捕获阶段被触发。

🧐 为什么有两个阶段?它们有什么用?

📌 历史渊源

在过去,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 (事件绑定的元素)
});

📌 事件对象中的targetcurrentTarget

在实际的使用中,你会发现事件对象中存在两个不同的属性:target currentTarget

它们有什么区别?和回调函数中的 this 的关系是怎样的?

复用上面验证捕获与冒泡顺序结论的例子,下面的代码片段验证了 targetcurrentTarget 的关系。

代码片段

当点击最内部的 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()
});

相关链接

事件冒泡及捕获

`,63),o=[p];function t(r,c,i,d,D,y){return n(),a("div",null,o)}const C=s(l,[["render",t]]);export{u as __pageData,C as default};