目录
事件监听 ( EventTarget.addEventListener() )
前言
javascript是事件驱动型语言。当用户在网页中进行某种操作时,就产生了一个“事件”(Event)。
事件几乎可以是任何事情:单击一个网页元素、拖动鼠标等均可视为事件。JavaScript是事件驱动的,当事件发生时,它可以对之做出响应。具体如何响应某个事件由编写的事件处理函数完成。
JavaScript 是一个事件驱动(Event-driven) 的语言,当浏览器载入网页开始读取后,虽然马上会读取JavaScript 事件相关的代码,但是必须要等到「事件」被触发(如使用者点击、按下键盘等)后,才会再进行对应代码段的执行。
啥意思呢?
就好比放了一部电话在家里,但是电话要是没响,我们不会主动去「接电话」 (没人打来当然也无法主动接) ,这里电话响了就好比事件被触发,接电话就好比去做对应的事情。
电话响了(事件被触发) -> 接电话(去做对应的事)
换以我们很常见的网页对话框UI 来说,当使用者「按下了按钮」之后,才会启动对话框的显示。如果使用者没有按下按钮,就狂跳对话框,那使用者一定觉得这网站瓦特了吧。
DOM事件流
事件流(Event Flow)指的就是「网页元素接收事件的顺序」。事件流可以分成两种机制:
- 事件捕获(Event Capturing)
- 事件冒泡(Event Bubbling)
当一个事件发生后,会在子元素和父元素之间传播(propagation)。这种传播分成三个阶段:
- 捕获阶段:事件从window对象自上而下向目标节点传播的阶段;
- 目标阶段:真正的目标节点正在处理事件的阶段;
- 冒泡阶段:事件从目标节点自下而上向window对象传播的阶段。
接着就来分别介绍事件捕获和事件冒泡这两种机制。
事件捕获(Event Capturing)
事件捕获指的是「从启动事件的元素节点开始,逐层往下传递」,直到最下层节点,也就是div。
假设HTML 如下:
<html>
<head>
<title>米淇淋是个大帅哥</title>
</head>
<body>
<div>点我</div>
</body>
</html>
假设我们点击(click)了<div>点我</div>
元素,那么在「事件捕获」的机制下,触发事件的顺序会是:
- document
<html>
<body>
<div>点我</div>
像这样click
事件由上往下依序被触发,就是「事件捕获」机制。
事件冒泡(Event Bubbling)
刚刚说过「事件捕获」机制是由上往下来传递,那么「事件冒泡」(Event Bubbling) 机制则正好相反。
假设HTML 同样如下:
<html>
<head>
<title>米淇淋是个大帅哥</title>
</head>
<body>
<div>点我</div>
</body>
</html>
假设我们点击(click)了<div>点我</div>
元素,那么在「事件冒泡」的机制下,触发事件的顺序会是:
<div>点我</div>
<body>
<html>
document
像这样click
事件逐层向上依序被触发,就是「事件冒泡」机制。
既然事件传递顺序有这两种机制,那我怎么知道事件是依据哪种机制执行的呢?
答案是:两种都会执行。
假设现在的事件是点击上图中蓝色的<td>
。
那么当td的click
事件发生时,会先走红色的「capture phase」:
Document
<html>
<body>
<table>
<tbody>
<tr>
<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
会先被触发,然后再到子层内部的Capturing
或Bubbling
事件。最后才又回到父层的Bubbling
结束。点击父元素的时候,不会经过子元素,子层的Capturing
和Bubbling
都不会触发。
那么,子层中的Capturing
或Bubbling
谁先谁后呢?要看你代码的顺序而定:
若是Capturing
在Bubbling
前面:
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'。因为addEventListener
与removeEventListener
所移除的函数实际上是两个不同实体的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);