JS事件冒泡、事件捕获以及事件委托

2,420 阅读5分钟

写在前面的话

事件委托是利用事件冒泡机制,在只指定一个事件处理程序的前提下就可以管理一类型的所有事件。网络上关于事件委托说的最后的一个例子就是取快递,是不是蜂巢也可以用来恰当的举例呢。

这个小区有3个住户,为了签收快递,有两种办法:一是每天就在家里等着快递员送货上门;二就是快递来了之后蜂巢代为签收。这样即使小区里又新加了住户的话,签收快递这件事情也一样可以交给蜂巢统一办理。

这里强调了两点:

1、现在委托蜂巢的这些住户的快递都是有快递需要代为签收的,即程序中的所有dom节点都是有事件的;

2、新住进来的住户的快递也是可以委托蜂巢的,也就是说,程序中新加的dom节点也是有事件的。

事件冒泡

当HTML中出现DOM结构的嵌套,并且子元素和父元素都绑定了相同的事件,这里以click事件为例。当最里面的子元素的点击事件被触发时,先触发子元素的时间处理器,再触发父元素的事件处理器,一直向上触发相同事件,直到document为止。这就像是鱼吐泡泡,从底下一直向上层跑,每经过一层就要检查是否有事件处理器,有的话就触发,没有就一直向上寻找。

事件捕获

事件捕获则和时间冒泡相反,点击的时候,从最外层向里逐层触发,直到点击位置的最底层,也就是通常说的事件的target。

在日常搬砖过程中,我们可以依据这两种方式的不同,实现产品的各种天花乱坠的需求。

在上篇中讲JS事件绑定的文章中,讲到了DOM2级事件绑定中,有第三个参数,这是一个布尔类型的值,决定的是这个事件的事件流处理方式。默认为false,表示事件流处理器是在冒泡阶段触发执行,当为true时,表示在捕获阶段执行。

<template>
  <div id="dom1">
    <div id="dom2">
      <div id="dom3"></div>
    </div>
  </div>
</template>
<script>
export default {
  mounted() {
    let dom1 = document.getElementById("dom1"),
      dom2 = document.getElementById("dom2"),
      dom3 = document.getElementById("dom3");
      //绑定事件,冒泡阶段执行
    dom1.addEventListener("click", this.func1, false);
    dom2.addEventListener("click", this.func2, false);
    dom3.addEventListener("click", this.func3, false);
  },
  methods: {
    func1() {
      console.log("点击dom1");
    },
    func2() {
      console.log("点击dom2");
    },
    func3() {
      console.log("点击dom3");
    }
  }
};


冒泡阶段执行,即从里向外执行。在这种情况下,点击区域内的任何地方,控制台输出的结果均为

image-20200609114731306
若改为捕获阶段执行的话,则为从外到里面执行。结果刚好相反:

//绑定事件,捕获阶段执行
    dom1.addEventListener("click", this.func1, true);
    dom2.addEventListener("click", this.func2, true);
    dom3.addEventListener("click", this.func3, true);
image-20200609114400007

用法

假设一种情况,在一个ul-li的DOM结构中,需要点击li时执行某个操作。这个时候,可以遍历所有li并绑定相关事件。但是,如果有100个,1000个li呢,难道要遍历1000次,进行1000次的事件绑定和事件移除吗?

这个时候就可以利用事件冒泡在外层ul上绑定点击事件,这样,在点击li时,外层ul上的点击事件将在冒泡阶段执行。轻松便捷,是我要的高质量代码。

阻止事件传播

面对千变化万,五花八门的需求,有些时候,我们不需要事件冒泡或者事件捕获。那么怎么去阻止事件传播呢?

想要阻止事件传播,首先要弄明白事件的传播机制。

事件传播机制

DOM2级事件规定,事件流包括三个阶段,事件捕获阶段(capturing-phase)、处于目标阶段(at-target)、事件冒泡阶段(bubbling-phase)。

当一个事件触发后,它会在不同节点之间传播(propagation)。当点击inner,触发了inner的click事件。浏览器在执行inner的click事件之前,

image-20200609154409347
  • 捕获阶段:从整个页面document开始向内查找,把inner的祖先全部遍历一遍(为冒泡阶段的传播路径做准备)

  • 目标阶段:找到事件源,将事件源上绑定的方法执行

  • 冒泡阶段:从目标节点传导回document。

    这种三阶段的传播模型,会使得一个事件在多个节点上触发。

    阻止事件传播

    event.stopPropagation()

    stopPropagation方法阻止事件在DOM中继续传播,即取消进一步的事件捕获或冒泡,防止再触发定义在别的节点上的监听函数,但是不包括在当前节点上新定义的事件监听函数。

    <template>
      <div id="dom1">
        <div id="dom2">
          <div id="dom3"></div>
        </div>
      </div>
    </template>
    <script>
    export default {
      mounted() {
        let dom1 = document.getElementById("dom1"),
          dom2 = document.getElementById("dom2"),
          dom3 = document.getElementById("dom3");
        dom1.addEventListener("click", this.func1, false);
        dom2.addEventListener("click", this.func2, false);
        dom3.addEventListener("click", this.func3, false);
      },
      methods: {
        func1(e) {
          console.log("点击dom1");
        },
        func2() {
          console.log("点击dom2");
        },
        func3(e) {
          console.log("点击dom3");
            //阻止事件传播
          e.stopPropagation();
        }
      }
    };
    </script>
    

    此时点击最里层dom,控制台的打印结果位:

    image-20200609170336490

写在后面

stopPropagation方法是个web API。当我们在平时开发过程中,框架给我们提供了便捷的事件绑定方法。例如,VUE中的v-bind:click指令。如果需要阻止事件传播,只需要v-bind:click.stop指令即可。