深入研究事件的冒泡和捕捉

169 阅读4分钟

如果一个元素和它的父元素有一个相同事件的处理程序,当被触发时,哪个元素会先触发?

在JavaScript中使用冒泡和捕获来传播事件为开发者提供了这个问题的答案。在这篇文章中,我们将学习事件冒泡和捕获是如何工作的,比较访问事件属性的不同方法,并通过一些不同的例子和用例。

让我们开始吧!

Event Capturing Bubbling Sequence Diagram

事件冒泡和捕获的顺序

什么是事件捕获?

在事件捕获中,也被称为涓涓细流,外部事件处理程序在特定处理程序启动之前就已经启动了。例如,div上的事件在按钮上的事件之前触发。

Event capturing hierarchy = document → htmlbody → parent → child

捕获的优先级比冒泡的高,这意味着捕获的事件处理程序在冒泡的事件处理程序之前执行,如事件传播的阶段所示。

  1. 捕获阶段:事件向元素下方移动
  2. 目标阶段:事件到达目标元素
  3. 冒泡阶段:事件从元素上冒出。

什么是事件冒泡?

事件冒泡遵循与事件捕获相反的顺序。一个事件从一个子HTML元素传播,然后在DOM层次结构中向上移动到其父元素。

Event bubbling hierarchy = child → parent → bodyhtml → document

倾听传播事件

我们可以使用addEventListener() 方法来监听这些传播事件,该方法被附加到HTML节点上。它接受三个参数:一个事件名称,一个回调函数和一个可选的捕获值,默认情况下,它被设置为false

 element.addEventListener(event, handler, false)

捕获值为空

让我们回顾一下,当捕获值留空时,如果用户点击按钮会发生什么。

element.addEventListener(event, handler)

点击事件在捕捉阶段开始。它在目标的父元素中搜索任何带有事件处理程序的事件。它不会找到捕获阶段的任何事件处理程序。

接下来,它向下滴到目标。一旦捕获阶段的所有事件被执行,事件就会进入冒泡阶段。它执行目标元素上设置的任何事件处理程序。它再次向上传播,在目标元素的父元素中寻找冒泡阶段的任何事件处理程序。现在,事件周期已经完成。

捕获值是true

让我们考虑一下如果捕获值被设置为true ,会发生什么。

element.addEventListener(event, handler, true)

上面的代码片段将遵循与该值为空时相同的顺序。关键的区别是,事件处理程序将在涓涓细流流向目标之前执行它发现的任何事件处理程序。

访问事件对象属性

处理程序用来访问事件对象属性的方法是有区别的。

  • event.target: 指向触发事件的DOM元素
  • event.eventPhase: 返回事件传播的当前阶段(捕获:1 ,目标:2 ,冒泡:3 )。
  • event.currentTarget: 指的是处理该事件的DOM元素

请注意,如果事件监听器被附加到父代,但事件传播被子代停止,event.currentTarget :指停止传播的DOM元素。

事件冒泡的结构

现在我们了解了事件冒泡和捕获的工作原理,让我们来试试一个例子假设我们有以下的DOM结构和以下的事件监听器,分别。

<button class="cta_button">Click me</button>


document 
.querySelector('.cta_button') 
.addEventListener('click', function(event) { 
  console.info(`Click event fired on ${this.nodeName}`);
 });

Click event fired on BUTTON ,将被记录到控制台。

嵌套的DOM结构

让我们看看当DOM结构被嵌套并使用附加在父元素上的相同事件监听器时会发生什么。

<div class="cta_container">
    <button class="cta_button">Watch me bubble</button>
</div>


document
.querySelector('.cta_container')
.addEventListener('click', function(event) {
  console.info(`Click event fired on ${this.nodeName}`);
});

在上面的代码片段中,我们在div ,即按钮的父元素上设置了一个点击事件监听器。当点击时,它记录了事件的类型和它所触发的元素。

当用户点击Watch me bubble 按钮时,事件会被引导到该按钮。如果为按钮设置了一个事件处理程序,事件就被触发了。否则,该事件会冒泡,或传播到父级div ,并在父级上触发一个点击事件。如果事件没有被处理,这个过程会继续到下一个父级的外部边界,直到最终到达文档对象。

即使你点击了按钮,记录到控制台的信息是Click event fired on DIV

给按钮附加事件监听器

如果我们也给按钮附加一个事件监听器会发生什么?

<div class="cta_container">
    <button class="cta_button">Watch me bubble</button>
</div>


document
.querySelector('.cta_container')
.addEventListener('click', function(event) {
    console.info(Click event fired on ${this.nodeName}`);
});

输出变成了Click event fired on BUTTONClick event fired on DIV

正如你所看到的,该事件冒泡到了父级。你可以使用event.bubbles 属性来确定一个事件是否冒泡。

document
.querySelector('.cta_button')
.addEventListener('click', function(event) {
    console.info(
  Click event fired on ${this.nodeName}. Does it bubble? ${event.bubbles}`
);
});

停止传播

一个DOM元素上的事件会传播到它所有的父元素,除非它被停止。虽然通常没有必要阻止冒泡,但在某些情况下,它可能是有用的。例如,停止传播可以防止事件处理程序之间的相互干扰。

考虑使用mousemovemouseup 事件来处理拖放。停止传播可以防止因用户随机移动鼠标而产生的浏览器错误。

在子元素上调用event.stopPropagation() ,可以防止它冒到父元素上。

document.querySelector('.cta_button').addEventListener('click', event => {
   event.stopPropagation();
   // ...
});

让我们为上一节的例子停止传播,我们点击了一个按钮。

<div class="cta_container">
  <button class="cta_button">Watch the bubble stop</button>
</div>

让我们添加事件监听器。

document
.querySelector('.cta_container')
.addEventListener('click', function(event) {
   console.info(`Click event fired on ${this.nodeName}`);
});

document
.querySelector('.cta_button')
.addEventListener('click', function(event) {
    event.stopPropagation();
  console.info(`Click event fired on ${this.nodeName}`);
});

使用event.stopPropagation() ,防止事件冒泡。输出变为Click event fired on BUTTON

防止浏览器默认

假设你想让传播继续下去,但你想阻止浏览器在没有监听器处理该事件的情况下执行其默认动作。你可以使用event.preventDefault()

document.querySelector('.cta_button')
.addEventListener('click', event => {
    event.preventDefault();
  // ...
});

事件冒泡的用例

让我们应用我们所涉及的内容,创建一个购物清单应用程序,一旦你购买了一个项目,就会打击它。在这种情况下,添加单独的事件监听器将是不可行的,因为你可能决定在未来添加一个新的项目。

相反,你需要将事件监听器附加到列表的父元素上。该事件监听器将通过事件冒泡来处理来自子元素的点击事件。

在下面的代码片段中,我们列出了我们的购物项目。

<ul class="list">
<li class="item">MacBook Pro</li>
<li class="item">Sony a6400 camera</li>
<li class="item">Boya universal cardioid microphone</li>
<li class="item">Light ring</li>
</ul>

添加一个连接到<ul> 的事件监听器,如下图所示。

document.querySelector('.list').addEventListener('click', event => {
  event.target.classList.toggle('purchased');
});

你可以在下面的CodePen中查看这段代码。

参见CodePen上Chiamaka Ikeanyi(@chiamakaikeanyi)
的笔
事件
冒泡。

事件捕获结构

在不支持事件冒泡的事件委托期间,事件捕获对于将事件附加到动态内容变得特别有利。例如,你可能需要处理像focusblur 这样不支持冒泡的事件。

为了在捕获阶段捕获事件,你需要将useCapture 选项设置为true 。记住,在默认情况下,它被设置为false

element.addEventListener(event, handler, true)

让我们考虑一个嵌套的DOM结构。

<div class="cta_container">
    <button class="cta_button">Watch me capture</button>
</div>

我们将设置父元素的useCapture 选项为true

document.querySelector('.cta_container').addEventListener(
'click', function(event) {
   console.info(Click event fired on ${this.nodeName}`);
},
true
);

document
.querySelector('.cta_button')
.addEventListener('click', function(event) {
   console.info(`Click event fired on ${this.nodeName}`);
});

与我们使用bubbling得到的相反,输出是Click event fired on DIVClick event fired on BUTTON

事件捕获的用例

让我们继续前面的例子,我们建立了一个购物清单。如果你想在购物清单中添加一个输入字段,使你能够为每个项目设置一个预算,那么附加在父类上的事件监听器将不适用于这些输入字段。

让我们来看看我们的购物清单和一个事件监听器的代码。

<h1 class="title">Shopping List</h1>
<ul class="list">
          <li class="item">
              MacBook Pro 
              <input class="budget" type="number" min=1>
          </li>
          <li class="item">
              Logitech MX Keys 
              <input class="budget" type="number" min=1>
          </li>
          <li class="item">
              Sony a6400 camera 
              <input class="budget" type="number" min=1>
          </li>
          <li class="item">
              Boya universal cardioid microphone 
             <input class="budget" type="number" min=1>
          </li>
          <li class="item">
              Light ring
              <input class="budget" type="number" min=1>
          </li>   
</ul>



document.querySelector(".list").addEventListener("focus", function (event) {
      console.info(${event.type} event fired on ${this.nodeName}`);
        event.target.style.background = "#eee";

       console.log("target:", event.target);
        console.log("currentTarget:", event.currentTarget);
        console.log("eventPhase:", event.eventPhase);
});

当你把光标集中在任何一个输入字段上时,什么也不会发生。然而,当你把useCapture 选项设置为true ,你就会达到预期的结果。

document.querySelector(".list").addEventListener("focus", function (event) {
      console.info(`${event.type} event fired on ${this.nodeName}`);
        event.target.style.background = "#eee";

        console.log("target:", event.target);
        console.log("currentTarget:", event.currentTarget);
        console.log("eventPhase:", event.eventPhase);
}, true);


请看CodePen上Chiamaka Ikeanyi(@chiamakaikeanyi)的Pen
事件捕获

你可以在上面的CodePen中查看这个列表。

结论

对事件冒泡和捕获的深刻理解是在JavaScript中处理用户事件的关键。在本教程中,我们学习了事件传播在JavaScript中是如何工作的,按照捕获、目标阶段和冒泡的顺序。

注意,冒泡总是从子元素传播到父元素,而捕捉则是从父元素传播到子元素。为了记住传播的顺序,你可以想到 "冒泡和涓流"。

我希望你喜欢这个教程!

The postDeep Dive into event bubbling and capturingappeared first onLogRocket Blog.