js的事件处理机制

目录

前言

DOM事件流

事件捕获(Event Capturing)

事件冒泡(Event Bubbling)

事件监听 ( EventTarget.addEventListener() )

前言

javascript是事件驱动型语言。当用户在网页中进行某种操作时,就产生了一个“事件”(Event)。
事件几乎可以是任何事情:单击一个网页元素、拖动鼠标等均可视为事件。JavaScript是事件驱动的,当事件发生时,它可以对之做出响应。具体如何响应某个事件由编写的事件处理函数完成。

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

啥意思呢?

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

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

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

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事件三部曲之事件机制的原理

  • 2
    点赞
  • 5
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值