温故而知新 - js事件流机制

Trying to understand some people is like trying to pick up turd by the clean end.

事件是javascript一个至关重要的部分,通过派遣/监听事件,可以实现很多动态功能。通过对事件机制的重温,总是能重新刷新一些曾经模糊的知识点。特别写此文给自己加深印象, 如有错误欢迎指正!

以下文章皆以click事件为例:

事件流机制

事件传递过程

事件从 用户行为触发执行完毕 共有三个状态: 捕获(1) - 抵达target元素(2) - 冒泡(3),由事件对象的eventPhase属性标识。看图:

https://www.w3.org/TR/DOM-Level-3-Events/images/eventflow.svg

假设用户点击了一个表格的单元格,按照上图序号表明的顺序, 用户点击鼠标的那一刻开始经历三个过程:

  • 从最外层的window到最内层的td元素按顺序一一捕获到click事件。此过程event.eventPhase=1。

    如果当前元素绑定了要在 捕获阶段执行 的监听函数(listener), 会在此过程开始执行。 (什么是 捕获阶段执行 的监听函数稍后再说)

  • 事件到达目标元素(可追溯到的最内层元素)。此时event.eventPhase=2。

  • 事件开始向上冒泡。 此时event.eventPhase=3。

    如果当前元素绑定了要在 冒泡阶段执行 的监听函数,会在此过程开始执行。

事件对象

说明两个概念: 事件对象(event)的两个属性:event.target 和 event.currentTarget。

  • event.target 是最终点击到的DOM元素, 在整个事件传递过程中不会变化。

  • event.currentTarget 是传递过程中,当前遍历到的元素。

    `

    <tbody>
      <tr>
        <td> table cell </td>
      </tr>
    </tbody>
    


    `

    如上代码(撇开body及以上的对象): 点击table cell区域, currentTarget依次是 table > tbody > tr > td(eventPhase=2) > tr > tbody > table,而target一直是td

绑定与监听

举个例子来说明事件的绑定和监听,方便理解上面的概念。

图例2

//example.html
<div id="d1">
  <div id="d2">
    <div id="d3">
      <div id="d4"></div>
    </div>
  </div>
</div>

// style.css
#d1 {
  width: 200px;
  height: 200px;
  background: aliceblue;
  margin: auto;
  text-align: center;
}
#d2 {
  width: 150px;
  height: 150px;
  background: antiquewhite;
}
#d3 {
  height: 100px;
  width: 100px;
  background: beige;
}
#d4 {
  width: 50px;
  height: 50px;
  background: azure;
}

//example.js
for(let elem of document.querySelectorAll("#d1, #d1 *")) {
  elem.addEventListener("click", e => console.log(`Capturing: ${elem.id}`), true);
  elem.addEventListener("click", e => console.log(`Bubbling: ${elem.id}`));
}

//output (点击d4区域)
Capturing: d1
Capturing: d2
Capturing: d3
Capturing: d4
Bubbling: d4
Bubbling: d3
Bubbling: d2
Bubbling: d1

绑定事件

有些js库封装了自己都事件绑定函数,这里不赘述,本质都是基于js原生都addEventListener方法。

addEventListener接收3个函数,前两个分别是事件类型和监听函数,不必多说。 注意第三个参数: 可以是Object / Boolean.

  • Boolean

    true - 表示指定都监听函数将在事件 捕获阶段 执行
    false(Default) - 表示监听函数在事件 冒泡阶段 执行

    这个值直接影响到不同元素对同一事件响应的作出先后顺序

  • Object

    {
      capture: Boolean,
      once: Boolean,
      passive: Boolean
    }
    

    这种形式不是本文的重点,详情可参考这里

target

在整个事件传递过程中,

如果用户点击d4区域:

  • currentTarget 的变化: d1 > d2 > d3 > d4 > d4 > d3 > d2 > d1
  • target: d4

    如果用户点击d2中不覆盖d3,d4的区域:

  • currentTarget 的变化: d1 > d2 > d2 > d1

  • target: d2

也就是说: event.target的值取决于事件触发时可追溯到的最内层元素。 点击d2中不覆盖d3,d4的区域,d2就是当前点击事件的最内层元素,所以event.target指向d2。

细枝末节

event对象有两个方法 preventDefaultstopPropergation:

  • event.preventDefault() - 取消事件的默认行为;

  • event.stopPropergation() - 阻止事件继续传递;

其中, preventDefault是取消事件的默认行为,但并不会阻止事件的传递,会继续 捕获 -> 抵到 -> 冒泡 的过程。 如下例:

<input id="checkbox" type='checkbox' value=true />

// js
var el = document.getElementById('checkbox');
el.addEventListener(‘click’, e => e.preventDefault());

点击这个checkbox的时候,正常情况checkbox应该被勾上, 但调用了preventDefault()方法,取消了该DOM元素点击事件的默认行为,checkbox就不会再勾选上。

stopPropergation是阻止事件继续往后传递。例如,

<input id="checkbox" type='checkbox' value=true />

// js
var el = document.getElementById('checkbox');
el.addEventListener('click', e => e.stopPropergation(), true);
el.addEventListener('click', e => console.log(el.tagName), true);

点击这个checkbox,由于在 捕获阶段 调用了stopPropergation(), 在此被截断,阻止了事件继续向后传递,根本不会有后面冒泡的过程,所以最后根本不会输出元素标签。通常元素的默认点击行为是在事件冒泡阶段才执行,所以这里虽然点击了checkbox,但仍然不会勾选上。

总结

  • 事件传递的三个状态: 捕获(1)、抵达目标(2)、冒泡(3),由事件对象Event的eventPhase属性标识;

  • Event.target始终指向事件触发时可追溯到的最深层元素;Event.currentTarget指事件传递过程中当前遍历到的元素;

  • Event.preventDefault() 取消事件的默认行为,但不会阻止传递; Event.stopPropergation()阻止事件继续向后传递;

参考资料