JavaScript 是一个事件驱动(Event-driven) 的语言,当浏览器载入网页开始读取后,虽然马上会读取 JavaScript 事件相关的代码,但是必须要等到「事件」被触发(如使用者点击、按下键盘等)后,才会再进行对应代码段的执行。

啥意思呢?

就好比放了一部电话在家里,但是电话要是没响,我们不会主动去「接电话」 (没人打来当然也无法主动接) ,这里电话响了就好比事件被触发,接电话就好比去做对应的事情。

电话响了(事件被触发) -> 接电话(去做对应的事)

换以我们很常见的网页对话框 UI 来说,当使用者「按下了按钮」之后,才会启动对话框的显示。如果使用者没有按下按钮,就狂跳对话框,那使用者一定觉得这网站瓦特了吧。

以 Bootstrap Modal 为例:

在上面的例子中,当使用者点击了按钮,才会启动对话框的显示,那么「点击按钮」这件事,就被称作「事件」(Event),而负责处理事件的代码段通常被称为「事件处理程序」(Event Handler),也就是「启动对话框的显示」这个动作。

看完上面的例子,想必大家对事件有了一定的理解了吧,接下来就深入来探讨 DOM 事件。

DOM 事件级别

DOM 有 4 次版本更新,与 DOM 版本变更,产生了 3 种不同的 DOM 事件:DOM 0 级事件处理,DOM 2 级事件处理和 DOM 3 级事件处理。由于 DOM 1 级中没有事件的相关内容,所以没有 DOM 1 级事件。

DOM 0 级事件

1.on-event (HTML 属性):

<input onclick="alert('xxx')" />

需要注意的是,基于代码的使用性与维护性考量,现在已经不建议用此方式来绑定事件。

on-event (非 HTML 属性):

像是windowdocument此类没有实体元素的情况:

window.onload = function () {
  document.write("Hello world!");
};

若是实体元素:

// HTML
<button id="btn">Click</button>

// JavaScript var btn = document.getElementById('btn'); btn.onclick =
function(){ alert('xxx'); }

若想解除事件的话,则重新指定on-eventnull即可:

btn.onclick = null;

2.同一个元素的同一种事件只能绑定一个函数,否则后面的函数会覆盖之前的函数

3.不存在兼容性问题

DOM 2 级事件

1.Dom 2 级事件是通过 addEventListener 绑定的事件

2.同一个元素的同种事件可以绑定多个函数,按照绑定顺序执行

3.解绑 Dom 2 级事件时,使用 removeEventListener

btn.removeEventListener("click", a);

Dom 2 级事件有三个参数:第一个参数是事件名(如click);第二个参数是事件处理程序函数;第三个参数如果是true的话表示在捕获阶段调用,为false的话表示在冒泡阶段调用。捕获阶段和冒泡阶段在下一节具体介绍。

还有注意removeEventListener():不能移除匿名添加的函数。

DOM 3 级事件

DOM3 级事件在 DOM2 级事件的基础上添加了更多的事件类型,增加的类型如下:

  • UI 事件,当用户与页面上的元素交互时触发,如:load、scroll
  • 焦点事件,当元素获得或失去焦点时触发,如:blur、focus
  • 鼠标事件,当用户通过鼠标在页面执行操作时触发如:dblclick、mouseup
  • 滚轮事件,当使用鼠标滚轮或类似设备时触发,如:mousewheel
  • 文本事件,当在文档中输入文本时触发,如:textInput
  • 键盘事件,当用户通过键盘在页面上执行操作时触发,如:keydown、keypress
  • 合成事件,当为 IME(输入法编辑器)输入字符时触发,如:compositionstart
  • 变动事件,当底层 DOM 结构发生变化时触发,如:DOMsubtreeModified
  • 同时 DOM3 级事件也允许使用者自定义一些事件。

DOM 事件流

事件流(Event Flow)指的就是「网页元素接收事件的顺序」。事件流可以分成两种机制:

  • 事件捕获(Event Capturing)
  • 事件冒泡(Event Bubbling)

当一个事件发生后,会在子元素和父元素之间传播(propagation)。这种传播分成三个阶段:

  1. 捕获阶段:事件从window对象自上而下向目标节点传播的阶段;
  2. 目标阶段:真正的目标节点正在处理事件的阶段;
  3. 冒泡阶段:事件从目标节点自下而上向window对象传播的阶段。

接着就来分别介绍事件捕获和事件冒泡这两种机制。

事件捕获(Event Capturing)

事件捕获指的是「从启动事件的元素节点开始,逐层往下传递」,直到最下层节点,也就是div

假设 HTML 如下:

<html>
  <head>
    <title>米淇淋是个大帅哥</title>
  </head>
  <body>
    <div>点我</div>
  </body>
</html>

假设我们点击(click)了<div>点我</div>元素,那么在「事件捕获」的机制下,触发事件的顺序会是:

  1. document
  2. <html>
  3. <body>
  4. <div>点我</div>

像这样click事件由上往下依序被触发,就是「事件捕获」机制。

事件冒泡(Event Bubbling)

刚刚说过「事件捕获」机制是由上往下来传递,那么「事件冒泡」(Event Bubbling) 机制则正好相反。

假设 HTML 同样如下:

<html>
  <head>
    <title>米淇淋是个大帅哥</title>
  </head>
  <body>
    <div>点我</div>
  </body>
</html>

假设我们点击(click)了<div>点我</div>元素,那么在「事件冒泡」的机制下,触发事件的顺序会是:

  1. <div>点我</div>
  2. <body>
  3. <html>
  4. document

像这样click事件逐层向上依序被触发,就是「事件冒泡」机制。

既然事件传递顺序有这两种机制,那我怎么知道事件是依据哪种机制执行的呢?

答案是:两种都会执行。

假设现在的事件是点击上图中蓝色的<td>

那么当 td 的click事件发生时,会先走红色的「capture phase」:

  1. Document
  2. <html>
  3. <body>
  4. <table>
  5. <tbody>
  6. <tr>
  7. <td> (实际被点击的元素)

由上而下依序触发它们的click事件。

然后到达「Target phase」后再继续执行绿色的「bubble phase」,反方向由<td>一路往上传至Document,整个事件流到此结束。

要检验事件流,我们可以通过addEventListener()方法来绑定click事件:

假设 HTML 如下:

<div>
  <div id="parent">
    父元素
    <div id="child">子元素</div>
  </div>
</div>

JavaScript 代码如下:

var parent = document.getElementById("parent");
var child = document.getElementById("child");

// 通过 addEventListener 指定事件的绑定
// 第三个参数 true / false 分別代表 捕获/ 冒泡 机制

parent.addEventListener(
  "click",
  function () {
    console.log("Parent Capturing");
  },
  true
);

parent.addEventListener(
  "click",
  function () {
    console.log("Parent Bubbling");
  },
  false
);

child.addEventListener(
  "click",
  function () {
    console.log("Child Capturing");
  },
  true
);

child.addEventListener(
  "click",
  function () {
    console.log("Child Bubbling");
  },
  false
);

当我点击的是「子元素」的时候,通过console.log可以观察到事件触发的顺序为:

"Parent Capturing";
"Child Capturing";
"Child Bubbling";
"Parent Bubbling";

而如果直接点击「父元素」,则出现:

"Parent Capturing";
"Parent Bubbling";

由此可知,点击子元素的时候,父层的Capturing会先被触发,然后再到子层内部的CapturingBubbling事件。最后才又回到父层的Bubbling结束。点击父元素的时候,不会经过子元素,子层的CapturingBubbling都不会触发。

那么,子层中的CapturingBubbling谁先谁后呢?要看你代码的顺序而定:

若是CapturingBubbling前面:

child.addEventListener(
  "click",
  function () {
    console.log("Child Capturing");
  },
  true
);

child.addEventListener(
  "click",
  function () {
    console.log("Child Bubbling");
  },
  false
);

则会得到:

"Child Capturing";
"Child Bubbling";

若是将两段代码段顺序反过来的话,就会是这样了:

child.addEventListener(
  "click",
  function () {
    console.log("Child Bubbling");
  },
  false
);

child.addEventListener(
  "click",
  function () {
    console.log("Child Capturing");
  },
  true
);

则会得到:

"Child Bubbling";
"Child Capturing";

事件监听 EventTarget.addEventListener()

addEventListener()基本上有三个参数,分别是「事件名称」、「事件的处理程序」(事件触发时执行的function),以及一个「Boolean」值,由这个 Boolean 决定事件是以「捕获」还是「冒泡」机制执行,若不指定则预设为「冒泡」。

// HTML
<button id="btn">Click</button>

// JavaScript var btn = document.getElementById('btn');
btn.addEventListener('click', function(){ console.log('HI'); }, false);

使用这种方式来注册事件的好处是:同一个元素的同种事件可以绑定多个函数,按照绑定顺序执行。

var btn = document.getElementById("btn");

btn.addEventListener(
  "click",
  function () {
    console.log("HI");
  },
  false
);

btn.addEventListener(
  "click",
  function () {
    console.log("HELLO");
  },
  false
);

点击后console出现:

"HI";
"HELLO";

若要解除事件的监听,则是通过removeEventListener()来取消。

removeEventListener()的三个参数与addEventListener()一样,分别是「事件名称」、「事件的处理程序」以及代表「捕获」或「冒泡」机制的「Boolean」值。

但是需要注意的是,由于addEventListener()可以同时针对某个事件绑定多个函数,所以通过removeEventListener()解除事件的时候,第二个参数的函数必须要与先前在addEventListener()绑定的函数是同一个「实体」。

比如:

var btn = document.getElementById("btn");

btn.addEventListener(
  "click",
  function () {
    console.log("HI");
  },
  false
);

// 移除事件,但是没用
btn.removeEventListener(
  "click",
  function () {
    console.log("HI");
  },
  false
);

像上面这样,即使执行了removeEventListener来移除事件,但click时仍会出现’HI’。因为addEventListenerremoveEventListener所移除的函数实际上是两个不同实体的 function 对象。

不知道为什么这两个 function 是两个不同实体的朋友请参考:《JavaScript 系列之内存空间》。简单理解就是两个 function 指向不同的内存地址,代表来自于不同实体。

稍加改进后就能如愿移除了:

var btn = document.getElementById("btn");

// 把 event 函数程序拉出來
var clickHandler = function () {
  console.log("HI");
};

btn.addEventListener("click", clickHandler, false);

// 移除 clickHandler, ok!
btn.removeEventListener("click", clickHandler, false);

那么以上就是今天为各位介绍 JavaScript 事件机制原理的部分。

接下来的文章我会继续来介绍事件的种类,以及更多实际上处理「事件」时需要注意的事项。

如果觉得文章对你有些许帮助,欢迎在我的 GitHub 博客点赞和关注,感激不尽!


JavaScript      JavaScript 事件 event

本博客所有文章除特别声明外,均采用 CC BY-SA 3.0协议 。转载请注明出处!