js 事件流的事件冒泡和事件捕获与阻止事件传播

为了方便引入事件流的概念,我们先来说说什么是事件。

事件就是用户或浏览器自身执行的某种动作。换句话说,我们在浏览网页或者 APP 时,通常会在设备上产生很多交互性的操作,例如点击、选择、滚动屏幕、键盘输入等等,这些交互性操作就是事件。

因为 DOM 结构是一个树型结构,所以当一个 HTML 元素产生一个事件时,该事件就会在元素结点与根节点之间按特定的顺序传播,路径所经过的节点都会收到该事件,这个事件传播的过程就被称为 事件流

但是,当一个 HTML 元素产生一个事件时,是该元素最外层的父元素先收到事件并执行,还是该元素本身先收到事件并执行?

关于这个问题,IE 和 Netscape 开发团队在开发初期提出了差不多完全相反的事件流概念。IE 的事件流是事件冒泡流,而 Netscape 的事件流是事件捕获流

事件冒泡

IE的事件流叫做事件冒泡(event bubbling)。冒泡,顾名思义,事件像个水中的气泡一样一直往上冒,直到顶端。即事件开始时由具体的元素(文档中嵌套层次最深的那个节点)接收,然后逐级向上传播到较为不具体的节点(文档)。以下面的 HTML 页面为例:

<!DOCTYPE html>
<html>
  <head>
  <title> Example </title>
  </head>
  <body>
    <div class="one"></div>
  </body>
</html>

如果你单击了页面中的<div>元素,那么这个 click 事件会按照如下顺序传播:

  1. <div>
  2. <body>
  3. <html>
  4. document

也就是说,click 事件首先在<div>元素上发生,而这个元素就是我们单击的元素。然后,click 事件沿 DOM 树向上传播,在每一级节点上都会发生,直至传播到 document 对象。下图展示了事件 冒泡的过程。

事件冒泡传播流程图示

事件捕获

Netscape 团队提出的另一种事件流叫做事件捕获(event capturing)。事件捕获的思想是不太具体的节点应该更早接收到事件,而具体的节点应该后接收到事件。事件捕获的用意在于在事件到达预定目标之前捕获它。如果仍以前面的 HTML页面作为演示事件捕获的例子,那么单击<div> 元素就会以下列顺序触发 click 事件。

  1. document
  2. <html>
  3. <body>
  4. <div>

在事件捕获过程中,document 对象首先接收到 click 事件,然后事件沿DOM树依次向下,一直 传播到事件的实际目标,即<div>元素。下图展现了事件捕获的过程。

事件捕获传播流程图示

DOM 事件流

后来,ECMAScript 标准重新将事件流规范为三个阶段,分别为事件捕获阶段,处于目标阶段,事件冒泡阶段。首先发生的是事件捕获,为截取事件提供了机会。然后是实际的目标接受到事件。最后一个阶段是冒泡阶段,可以在这个阶段对事件做出响应。以前面的 HTML 页面为例。点击<div>元素会按照下图所示顺序触发事件。

DOM 事件流的三个阶段

虽然大部分的浏览器都遵循着标准,但是在IE浏览器中,事件流却是非标准的。在IE中事件流只有两个阶段:处于目标阶段,冒泡阶段。如下图所示:

IE 的 DOM 事件流只有两个阶段

还有另外一个需要注意的地方,标准的事件流中所有的事件都要经过捕捉阶段和目标阶段,但是有些事件会跳过冒泡阶段。例如,让元素获得输入焦点的 focus 事件以及失去输入焦点的 blur 事件就都不会冒泡。

阻止事件流

为什么要阻止事件流

事件就是用户或浏览器自身执行的某种动作。诸如 点击、加载 和鼠标滑过。而响应某个事件的函数就叫做事件处理函数(又叫事件监听函数、事件句柄)。

因为事件流的存在,当某个元素的父元素或者子元素与当前元素拥有相同的事件处理函数时一个问题就暴露了出来。

为了更好的说明这个问题,我们把前面的那个简单的 HTML 拿来重新改造一下。

<!DOCTYPE html>
<html>
<head>
  <title> Example </title>
  <style>
    .one {
      width: 100px;
      height: 100px;
      background-color: #00F;
      cursor: pointer;
    }
  </style>
</head>
<body>
  <div class="one"></div>
  <script>
    const html = document.querySelector('html');
    const body = document.body;
    const div = document.querySelector('.one');

    document.onclick = Event => {
      alert("我是:document");
    }
    html.onclick = Event => {
      alert("我是:html");
    }
    body.onclick = Event => {
      alert("我是:body");
    }
    div.onclick = Event => {
      alert("我是:div");
    }
  </script>
</body>
</html>

我们期望的是当点击 div 时弹出 “我是:div”,然而...

事件冒泡

这就是事件流附带的一个弊端,在事件冒泡阶段, <div> 的 click 事件向上传播到根节点,除了触发自己的事件处理函数之外,还顺带触发了上级元素上的 click 事件处理函数。

也就是说,当一个元素与父元素或祖元素拥有相同的事件,该元素的事件处理函数被触发时,父元素和祖元素相同的事件也会被触发。

如何阻止事件流

换句话说,为了避免页面交互操作出现异常,当一个元素设置一个事件后,它的子元素或者父元素就不能设置同样的事件。很显然,这样的结果并不是我们想要的。难道就没有什么解决的方法么?答案是肯定的:有。

W3C 中有一个 stopPropagation() 事件方法。该方法将停止事件的传播,阻止它被分派到其他 Document 节点。但是这是一个 DOM 2 级方法,IE 不出意外的不支持,IE 的方法是 e.cancelBubble=true

我们把上边示例的 javascript 代码拿过来改一下,看看效果如何。

  <script>
    const html = document.querySelector('html');
    const body = document.body;
    const div = document.querySelector('.one');

    html.onclick = Event => {
      window.event ? window.event.cancelBubble = true : e.stopPropagation();
      alert("我是:html");
    }
    body.onclick = Event => {
      window.event ? window.event.cancelBubble = true : e.stopPropagation();
      alert("我是:body");
    }
    div.onclick = Event => {
      window.event ? window.event.cancelBubble = true : e.stopPropagation();
      alert("我是:div");
    }
  </script>

阻止事件冒泡

不错,这才是我们想要的效果。

参考

  1. JavaScript高级程序设计(第3版)尼古拉斯 · 泽拉斯 著
  2. W3C--TML DOM Event 对象:http://www.w3school.com.cn/jsref/dom_obj_event.asp
  3. 浅谈前端和移动端的事件机制:https://juejin.im/post/59ede91ef265da43143fde87#heading-1

猜你喜欢

转载自www.cnblogs.com/wangdapang/p/10500602.html
今日推荐