bubbling and capturing

bubbling

  • stopPropagation prevents further propagation of the current event in the capturing and bubbling phases.
    如果一个元素在一个事件上有多个处理程序,即使其中一个停止冒泡,其他处理程序仍会执行。
    换句话说,event.stopPropagation() 停止向上移动,但是当前元素上的其他处理程序都会继续运行。
    有一个 event.stopImmediatePropagation() 方法,可以用于停止冒泡,并阻止当前元素上的处理程序运行。使用该方法之后,其他处理程序就不会被执行。

  • preventDefault prevents the default action the browser makes on that event.

  • 使用 css pointer-events: none 实现点击穿透

html
1
2
3
4
5
6
7
8
9
 <div class='parent' onclick='alert(`parent`)'>
   <div class='child' onclick='clickChild(event)'>child</div>
 </div>
  <script>
    function clickChild(e) {
      alert('child');
      e.stopPropagation();
    }
  </script>
  • stopImmediatePropagation 如果多个事件监听器被附加到相同元素的相同事件类型上,当此事件触发时,它们会按其被添加的顺序被调用。 如果在其中一个事件监听器中执行 stopImmediatePropagation() ,那么剩下的事件监听器都不会被调用,还会阻止冒泡。

不必要的 stopPropagation

不要在没有需要的情况下停止冒泡!
冒泡很方便。不要在没有真实需求时阻止它:除非是显而易见的,并且在架构上经过深思熟虑的。
有时 event.stopPropagation() 会产生隐藏的陷阱,以后可能会成为问题。
例如:

  1. 我们创建了一个嵌套菜单,每个子菜单各自处理对自己的元素的点击事件,并调用 stopPropagation,以便不会触发外部菜单。
  2. 之后,我们决定捕获在整个窗口上的点击,以追踪用户的行为(用户点击的位置)。有些分析系统会这样做。通常,代码会使用 document.addEventListener('click'…) 来捕获所有的点击。
  3. 我们的分析不适用于被 stopPropagation 所阻止点击的区域。太伤心了,我们有一个“死区”。

通常,没有真正的必要去阻止冒泡。一项看似需要阻止冒泡的任务,可以通过其他方法解决。其中之一就是使用自定义事件,稍后我们会介绍它们此外,我们还可以将我们的数据写入一个处理程序中的 event 对象,并在另一个处理程序中读取该数据,这样我们就可以向父处理程序传递有关下层处理程序的信息。

capturing

DOM 事件标准描述了事件传播的 3 个阶段:

  1. 捕获阶段(Capturing phase)—— 事件(从 Window)向下走近元素。
  2. 目标阶段(Target phase)—— 事件到达目标元素。
  3. 冒泡阶段(Bubbling phase)—— 事件从元素上开始冒泡。

使用 on<event> 属性或使用 HTML 特性(attribute)或使用两个参数的 addEventListener(event, handler) 添加的处理程序,对捕获一无所知,它们仅在第二阶段和第三阶段运行。
为了在捕获阶段捕获事件,我们需要将处理程序的 capture 选项设置为 true

js
1
2
3
elem.addEventListener(..., {capture: true})
// 或者,用 {capture: true} 的别名 "true"
elem.addEventListener(..., true)

bubbling vs capturing

html
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
<style>
  body * {
    margin: 10px;
    border: 1px solid blue;
  }
</style>

<form>FORM
  <div>DIV
    <p>P</p>
  </div>
</form>

<script>
  for(let elem of document.querySelectorAll('*')) {
    elem.addEventListener("click", e => alert(`Capturing: ${elem.tagName}`), true);
    elem.addEventListener("click", e => alert(`Bubbling: ${elem.tagName}`));
  }
</script>

上面这段代码为文档中的 每个 元素都设置了点击处理程序,以查看哪些元素上的点击事件处理程序生效了。

如果你点击了 <p>,那么顺序是:

  1. HTMLBODYFORMDIV(捕获阶段第一个监听器):
  2. P(目标阶段,触发两次,因为我们设置了两个监听器:捕获和冒泡)
  3. DIVFORMBODYHTML(冒泡阶段,第二个监听器)。

每个处理程序都可以访问 event 对象的属性:

  • event.target —— 引发事件的层级最深的元素。
  • event.currentTarget(=this)—— 处理事件的当前元素(具有处理程序的元素)
  • event.eventPhase —— 当前阶段(capturing=1,target=2,bubbling=3)。