在做前端开发的时候,咱们常常须要做一些各式各样的交互,如鼠标单击 / 双击 / 滑动事件、键盘事件等等等等,这些都是 DOM 事件。首先咱们先看一个概念,叫 DOM 事件流。
DOM 事件流
事件流:事件在指标元素和先人元素间的触发程序。
学习前端的同学对于事件流必定不会生疏,不就是冒泡和捕捉?是的,事件不会只作用在绑定的元素上,而是会有一个流向,流向上的 DOM 元素绑定了事件的话,都会响应。在晚期,微软和网景实现了相同的事件流,网景主张捕捉形式,微软主张冒泡形式。
- 捕捉(Capture):事件由最顶层逐级向下流传, 直至达到指标元素。
- 冒泡(Bubble):事件由第一个被触发的元素接管, 而后逐级向上流传。
最初 W3C 采纳折中的形式,规定先捕捉再冒泡,才平息了战火。如此一个事件就被分成了三个阶段(是的,不光是捕捉和冒泡):
- 捕捉阶段:事件从最顶层元素 window 始终传递到指标元素的父元素。
- 指标阶段:事件达到指标元素,如果事件指定不冒泡,那就会在这里停止。
- 冒泡阶段:事件从指标元素父元素向上逐级传递直到最顶层元素 window。
也就是如下图所示,这里只需有个大略的概念,前面将具体介绍这几个阶段:
好了,对事件流有了一个大略的了解了,咱们再来看看 DOM 级别。
DOM 级别
DOM 级别是啥玩意呢?其实就是 DOM 在不同期间进去的一些标准,就像 JavaScript 里的 ES5、ES6、ES7 等是一个意思,一直往里面增加一些新的货色。DOM 级别有 DOM0、DOM1、DOM2、DOM3。其中 DOM1 和事件没有关系,将不做介绍。
DOM0 级事件
为啥要以 0 为终点呢?因为这不是 W3C 标准。最开始的时候还没有标准,但各个浏览器就约定俗成的这么做了,所以就成了一个事实上的“标准”。
那么什么是 DOM0 级处理事件呢?DOM0 级事件就是将一个函数赋值给一个事件处理属性,比方:
<button id="btn" type="button"> 点我 </button>
<script>
var btn = document.getElementById('btn');
btn.onclick = function() {alert('Hello World');
}
// btn.onclick = null; 解绑事件
</script>
以上代码咱们给 button 定义了一个 id,通过 JS 获取到了这个 id 的按钮,并将一个函数赋值给了一个事件处理属性 onclick,这样的办法便是 DOM0 级处理事件的体现。咱们能够通过给事件处理属性赋值 null 来解绑事件。
DOM0 级事件处理程序的毛病在于一个处理程序 无奈同时绑定多个处理函数,比方我还想在按钮点击事件上加上另外一个函数。
DOM2 级事件
DOM2 级事件在 DOM0 级事件的根底上补救了无奈同时绑定多个处理函数的毛病,容许给一个 DOM 元素增加多个处理函数(addEventListener)。运行如下代码后点击按钮,咱们能够发现两个事件均会被触发:
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>Document</title>
</head>
<body>
<button id="btn" type="button"> 点我 </button>
<script>
var btn = document.getElementById('btn');
function showFn1() {alert('Hello1');
}
function showFn2() {alert('Hello2');
}
btn.addEventListener('click', showFn1, false);
btn.addEventListener('click', showFn2, false);
</script>
</body>
</html>
DOM2 级事件定义了 addEventListener
和 removeEventListener
两个办法,别离用来绑定和解绑事件,办法中蕴含 3 个参数,别离是绑定的事件处理属性名称(留神不蕴含 on)、处理函数和是否在捕捉阶段执行。
DOM3 级事件
DOM3 级事件在 DOM2 级事件的根底上增加了更多的事件类型,全副类型如下:
- UI 事件,当用户与页面上的元素交互时触发,如:load、scroll
- 焦点事件,当元素取得或失去焦点时触发,如:blur、focus
- 鼠标事件,当用户通过鼠标在页面执行操作时触发如:dbclick、mouseup
- 滚轮事件,当应用鼠标滚轮或相似设施时触发,如:mousewheel
- 文本事件,当在文档中输出文本时触发,如:textInput
- 键盘事件,当用户通过键盘在页面上执行操作时触发,如:keydown、keypress
- 合成事件,当为 IME(输入法编辑器)输出字符时触发,如:compositionstart
- 变动事件,当底层 DOM 构造发生变化时触发,如:DOMsubtreeModified
同时 DOM3 级事件也容许使用者自定义一些事件。
详解事件流
事件冒泡
所谓事件冒泡就是事件像泡泡一样从最开始生成的中央一层一层往上冒,比方上图中 a 标签为事件指标,点击 a 标签后同时也会触发 p、li 上的点击事件,一层一层向上直至最外层的 html 或 document。上面是代码示例:
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Document</title>
</head>
<body>
<p id="parent" style="background-color: red;padding: 20px">
<a id="child" style="background-color: blue;"> 事件冒泡 </a>
</p>
<script>
var parent = document.getElementById('parent');
var child = document.getElementById('child');
child.addEventListener('click', function () {alert('我是指标 a');
}, false);
parent.addEventListener('click', function () {alert('事件冒泡至 p');
}, false);
</script>
</body>
</html>
下面的代码运行后咱们点击 a 标签,首先会弹出【我是指标 a】的提醒,而后又会弹出【事件冒泡至 p】的提醒,这便阐明了事件自内而外向上冒泡了。
事件捕捉
和事件冒泡相同,事件捕捉是自上而下执行,咱们只须要将 addEventListener
的第三个参数改为 true 就行。同样是下面的例子,咱们在 p 标签上增加一个捕捉阶段的绑定,代码如下:
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Document</title>
</head>
<body>
<p id="parent" style="background-color: red;padding: 20px">
<a id="child" style="background-color: blue;"> 事件冒泡 </a>
</p>
<script>
var parent = document.getElementById('parent');
var child = document.getElementById('child');
// 增加此捕捉阶段的事件
parent.addEventListener('click', function () {alert('事件捕捉至 p');
}, true);
child.addEventListener('click', function () {alert('我是指标 a');
}, false);
parent.addEventListener('click', function () {alert('事件冒泡至 p');
}, false);
</script>
</body>
</html>
下面的代码运行后咱们点击 a 标签,提醒程序为:【事件捕捉至 p】->【我是指标 a】->【事件冒泡至 p】。能够看到,事件的确是如上图所示,先进行捕捉,后冒泡。
终止事件流传
不论在捕捉阶段还是冒泡阶段,咱们都不心愿事件再进行传递了,那么咱们怎么进行阻止呢?这时咱们能够应用Event
对象的 stopPropagation
办法。而 Event
怎么获取呢?其实每个事件响应函数都有一个参数,就是这个事件对象。
同样是下面的例子,当点击 a 标签,若咱们在 p 标签的捕捉阶段就阻止事件的传递,这样一来前面的事件均不会执行,代码示例如下:
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Document</title>
</head>
<body>
<p id="parent" style="background-color: red;padding: 20px">
<a id="child" style="background-color: blue;"> 事件冒泡 </a>
</p>
<script>
var parent = document.getElementById('parent');
var child = document.getElementById('child');
parent.addEventListener('click', function (e) {alert('事件捕捉至 p');
// 阻止流传,"我是指标 a" 和 "事件冒泡至 p" 都不执行
e.stopPropagation();}, true);
child.addEventListener('click', function () {alert('我是指标 a');
// 阻止流传,"事件冒泡至 p" 不执行
// e.stopPropagation();}, false);
parent.addEventListener('click', function () {alert('事件冒泡至 p');
}, false);
</script>
</body>
</html>
运行点击 a 标签,咱们能够看到如正文那样的成果。只有看明确了事件流的那张图,理解了捕捉 / 指标 / 冒泡这三个阶段,其实就很容易了解了 stopPropagation
阻止事件流所引起的这些景象。这里须要留神的是 stopPropagation
这个函数是阻止事件流传(从单词中能够明确),而不单单只是阻止冒泡,在其余的文章中没有解释分明。
疑难❓
onclick 是在哪些阶段执行?
这里咱们针对的是 非指标阶段,批改上述例子,咱们 id=parent 节点,即 p 标签增加一个 onclick 事件。
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Document</title>
</head>
<body>
<p id="parent" style="background-color: red;padding: 20px">
<a id="child" style="background-color: blue;"> 事件冒泡 </a>
</p>
<script>
var parent = document.getElementById('parent');
var child = document.getElementById('child');
parent.addEventListener('click', function (e) {alert('事件捕捉至 p');
}, true);
child.addEventListener('click', function () {alert('我是指标 a');
}, false);
parent.addEventListener('click', function () {alert('事件冒泡至 p');
}, false);
parent.onclick = function (e) {alert('p 标签 onclick');
}
</script>
</body>
</html>
咱们能够看到会有这样的一个程序:
【事件捕捉至 p】->【我是指标 a】->【事件冒泡至 p】->【p 标签 onclick】
从下面的后果咱们能够得出结论:onclick 是在冒泡阶段执行的!!!
那么另外一个问题来了,既然咱们在 p 标签的冒泡阶段绑定了两个 click 函数,一个是通过 addEventListener(DOM2 级别)增加,一个是间接给 onclick 属性赋值(DOM0 级别),那么这两个函数哪个会先执行呢?间接给出论断,和绑定的程序无关,也就是说 谁写在后面就先执行谁。同样是下面的例子,咱们 p 标签绑定的两个函数对调一下先后地位:
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Document</title>
</head>
<body>
<p id="parent" style="background-color: red;padding: 20px">
<a id="child" style="background-color: blue;"> 事件冒泡 </a>
</p>
<script>
var parent = document.getElementById('parent');
var child = document.getElementById('child');
parent.addEventListener('click', function (e) {alert('事件捕捉至 p');
}, true);
child.addEventListener('click', function () {alert('我是指标 a');
}, false);
parent.onclick = function (e) {alert('p 标签 onclick');
}
parent.addEventListener('click', function () {alert('事件冒泡至 p');
}, false);
</script>
</body>
</html>
咱们能够看到会变成这样的一个程序:
调整后:【事件捕捉至 p】->【我是指标 a】->【p 标签 onclick】->【事件冒泡至 p】
调整前:【事件捕捉至 p】->【我是指标 a】->【事件冒泡至 p】->【p 标签 onclick】
可见,正是印证了咱们之前的论断。
那么在指标阶段呢?即 a 标签咱们既绑定了 onclick 事件,又通过 addEventListener 绑定了 click 事件,那么这个时候会按什么程序执行呢?因为这个时候是在指标阶段,那么执行程序还是和上述一样,先绑定先执行。
总结成一句话:先绑定先执行,onclick 在冒泡阶段执行。