![93b3dfe30044d714e0bc562c894ef773.png](https://i-blog.csdnimg.cn/blog_migrate/f56a376fbeff222f3ac2138f34e9e1cb.png)
JavaScript 是一个事件驱动(Event-driven) 的语言,当浏览器载入网页开始读取后,虽然马上会读取JavaScript 事件相关的代码,但是必须要等到「事件」被触发(如使用者点击、按下键盘等)后,才会再进行对应代码段的执行。
啥意思呢?
就好比放了一部电话在家里,但是电话要是没响,我们不会主动去「接电话」 (没人打来当然也无法主动接) ,这里电话响了就好比事件被触发,接电话就好比去做对应的事情。
电话响了(事件被触发) -> 接电话(去做对应的事)
换以我们很常见的网页对话框UI 来说,当使用者「按下了按钮」之后,才会启动对话框的显示。如果使用者没有按下按钮,就狂跳对话框,那使用者一定觉得这网站瓦特了吧。
以Bootstrap Modal 为例:
![7184f340b0ac06abce9700282db068e1.gif](https://i-blog.csdnimg.cn/blog_migrate/e1cb3bf1bd6d0dea795e85e5598cd252.gif)
在上面的例子中,当使用者点击了按钮,才会启动对话框的显示,那么「点击按钮」这件事,就被称作「事件」(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 属性):
像是window
或document
此类没有实体元素的情况:
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-event
为null
即可:
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)。这种传播分成三个阶段:
- 捕获阶段:事件从window对象自上而下向目标节点传播的阶段;
- 目标阶段:真正的目标节点正在处理事件的阶段;
- 冒泡阶段:事件从目标节点自下而上向window对象传播的阶段。
接着就来分别介绍事件捕获和事件冒泡这两种机制。
事件捕获(Event Capturing)
![8711ab88a08c9eadd6870308a11ec5c7.png](https://i-blog.csdnimg.cn/blog_migrate/c01ff6b7355d1976cc613d3c2ff5e9a8.png)
事件捕获指的是「从启动事件的元素节点开始,逐层往下传递」,直到最下层节点,也就是div。
假设HTML 如下:
<html>
<head>
<title>米淇淋是个大帅哥</title>
</head>
<body>
<div>点我</div>
</body>
</html>
假设我们点击(click)了<div>点我</div>
元素,那么在「事件捕获」的机制下,触发事件的顺序会是:
document
<html>
<body>
<div>点我</div>
像这样click
事件由上往下依序被触发,就是「事件捕获」机制。
事件冒泡(Event Bubbling)
![6c4d4875c78a43156a98e9533cfcd141.png](https://i-blog.csdnimg.cn/blog_migrate/36b38737e25b48f7e796c0cfcc05afea.png)
刚刚说过「事件捕获」机制是由上往下来传递,那么「事件冒泡」(Event Bubbling) 机制则正好相反。
假设HTML 同样如下:
<html>
<head>
<title>米淇淋是个大帅哥</title>
</head>
<body>
<div>点我</div>
</body>
</html>
假设我们点击(click)了<div>点我</div>
元素,那么在「事件冒泡」的机制下,触发事件的顺序会是:
<div>点我</div>
<body>
<html>
document
像这样click
事件逐层向上依序被触发,就是「事件冒泡」机制。
既然事件传递顺序有这两种机制,那我怎么知道事件是依据哪种机制执行的呢?
答案是:两种都会执行。
![4d66f1b95dbe269d2c692cd23c255da1.png](https://i-blog.csdnimg.cn/blog_migrate/082bed1e45a247ef19b52e97db4c79e5.jpeg)
假设现在的事件是点击上图中蓝色的<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);
那么以上就是今天为各位介绍JavaScript事件机制原理的部分。
接下来的文章我会继续来介绍事件的种类,以及更多实际上处理「事件」时需要注意的事项。
如果觉得文章对你有些许帮助,欢迎在我的GitHub博客点赞和关注,感激不尽!