DOM系列:DOM事件的传播
通过前面的DOM事件模型和事件绑定的姿势两节的学习,我们对JavaScript中的DOM事件有了一定的基础。但忽略了有关事件如何被触发的重要细节。
事件传播
让我们从事件传播开始。事件传播是事件冒泡和事件捕获的总称。先来看一张图,这张图在前面的文章中有也出现过,但没有深入的介绍。先上图吧!
W3C规范中定义了三个事件阶段,依次是捕获阶段、目标阶段和冒泡阶段。事件对象按照上图的传播路径依次完成这些阶段。如果某个阶段不支持或事件对象的传播被终止,那么该阶段就会被跳过。详细的阐述,我们后面会通过示例来向大家描述,这样会更易于理解。这里先来了解这三个事件阶段的概念:
- 捕获阶段:在事件对象到达事件目标之前,事件对象必须从
window
经过目标的祖先节点传播到事件目标。这个阶段被我们称之为捕获阶段。在这个阶段注册的事件监听器在事件到达其目标前必须先处理事件 - 目标阶段:事件对象到达其事件目标。这个阶段被我们称为目标阶段。一旦事件对象到达事件目标,该阶段的事件监听器就要对它进行处理。如果一个事件对象类型被标志为不能冒泡。那么对应的事件对象在到达此阶段时就会终止传播
- 冒泡阶段:事件对象以一个与捕获阶段相反的方向从事件目标传播经过其祖先节点传播到
window
。这个阶段被称之为冒泡阶段。在此阶段注册的事件监听器会对相应的冒泡事件进行处理
理解概念总是很累的。为了更好的了解事件及其工作细节,我们来创建一个简单的示例,通过示例来帮助我们理解事件的工作细节,比如前面提到的事件传播。
先来创建示例所需要的HTML结构:
<body id="theBody" class="item">
<div id="one_a" class="item">
<div id="two" class="item">
<div id="three_a" class="item">
<button id="buttonOne" class="item">one</button>
</div>
<div id="three_b" class="item">
<button id="buttonTwo" class="item">two</button>
<button id="buttonThree" class="item">three</button>
</div>
</div>
</div>
<div id="one_b" class="item">
</div>
</body>
正如你所看到的,这里没有什么特别之处。HTML结构很简单,我们用前面学到的知识,把上面的HTML结构用DOM树描绘出来,将会像下面这样:
假设我们点击了buttonOne
元素。从前面学到的知识中我们知道,这将会触发一个click
事件。click
事件实际上并不源于你与之交互的元素。因为事件会从你的文档开始:
从根开始,事件从上一级一级往下,并在触发事件的元素buttonOne
(事件目标)处停止:
如图所示,click
事件所采用的路径是直接的,他会通知该路径中的每个元素。这也意味着,你要监听body
、one_a
、two
、three_a
上的click
事件,也就会触发相关的事件处理程序。这是一个很重要的细节。稍后我们再详细来介绍。
现在,一旦你的事件达到目标,它就不会停止。它会重新追溯它的步骤,回到根源:
就像以前一样,在事件向上移动时,事件路径上的每个元素都会被通知它的存在。
其实这两个过程,前者通常被称为事件捕获,而后者被称为事件冒泡。接下来我们来聊聊事件捕获和事件冒泡的一些细节。
事件捕获
有一个细节我们需要注意,在DOM中启动事件的位置并不重要。因为事件始终从根开始,沿着路径直到达到目标,然后再重新追溯根源,然后回到根。而其中启动事件的部分和事件从根向下阻止DOM称为事件捕获阶段:
在此阶段,仅调用捕获监听器,也就是addEventListener
的第三个参数为true
:
element.addEventListener('click', listener, true)
如果省略此参数,则其默认值为false
,并且监听器不是捕获器,而变成冒泡。因此,在此阶段期间,仅调用从window
到事件目标父级的路径上找到的捕获器。
回到我们的示例中:
Array.from(document.querySelectorAll('.item')).forEach(item => {
var id = item.id;
item.addEventListener('click',function(e){
console.log(`${e.type}: ${id}`)
}, true);
})
点击不同的元素,可以看到事件捕获的过程:
具体的Demo,可以点击这里,打开浏览器开发者工具,你点击不同的元素时,在console
项中会输出对应的事件捕获过程。
事件冒泡
上面看到的是事件传播的第一阶段,即事件捕获阶段。接下来是看另一个阶段,即事件冒泡阶段:
在事件冒泡阶段,只会调用非捕获者。也就是说,只有addEventListener
的第三个参数的值为false
的事件监听器。比如上面的示例,把addEventListener
的第三个参数设置为false
,或者不显式的设置(因为其默认值为false
):
具体的Demo,可以点击这里,打开浏览器开发者工具,你点击不同的元素时,在console
项中会输出对应的事件冒泡过程。
事件中断
现实中,很多时候我们并不希望目标元素的事件结束之后还去追溯其根源(冒泡)。也就是想在需要的地方可以结束事件的生命。在JavaScript中可以在事件对象上使用stopPropagation
方法:
function handleClick(e) {
e.stopPropagation();
// do something
}
也就是说,stopPropagation
方法可以阻止事件在各个阶段中运行。来看一个示例,比如你在three_a
元素有一个click
事件,并且希望阻止事件传播。比如像下面这样:
Array.from(document.querySelectorAll('.item')).forEach(item => {
var id = item.id;
item.addEventListener('click',function(e){
console.log(`${e.type}: ${id}`)
}, true);
})
Array.from(document.querySelectorAll('.item')).forEach(item => {
var id = item.id;
item.addEventListener('click',function(e){
console.log(`${e.type}: ${id}`)
}, false);
})
var theElement = document.querySelector("#three_a");
theElement.addEventListener("click", doSomething, true);
function doSomething(e) {
e.stopPropagation();
}
当你点击buttonOne
元素时,事件的路径变成下面这样:
按理说,click
事件捕获会从window
一级一级向下移动DOM树,并且会通知到路径上的每个DOM元素,直到目标元素buttonOne
停止。但上面的代码,我们在three_a
元素的click
事件的捕获监听事件doSomething
时调用了stopPropagation
方法。事件的捕获到three_a
将会停止。来看一下添加stopPropagation
方法前后的效果:
从上图我们可以看到,此时你虽然点击了buttonOne
元素,但程序却无法捕获到该元素的上的click
事件。而且事件也不会再冒泡到根元素。
停止即时传播
正如其名称的含义,stopImmediatePropagation
会立即停止,甚至阻止了当前监听器的兄弟姐妹接收事件。
如果有多个相同类型事件的事件监听函数绑定到同一个元素,当该类型的事件触发时,它们会按照被添加的顺序执行。如果其中某个监听函数执行了 event.stopImmediatePropagation()
方法,则当前元素剩下的监听函数将不会被执行。
来看下个简单的示例:
let buttonOneEle = document.getElementById('buttonOne')
// buttonOne 绑定的第一个监听函数
buttonOneEle.addEventListener("click", (event) => {
console.log(`第一 ${event.type}: ${event.target.id}`);
}, false);
// buttonOne 绑定的第二个监听函数
buttonOneEle.addEventListener("click", (event) => {
console.log(`第二 ${event.type}: ${event.target.id}`);
//执行stopImmediatePropagation方法,阻止click事件冒泡,并且阻止p元素上绑定的其他click事件的事件监听函数的执行
event.stopImmediatePropagation();
}, false);
// 该监听函数排在上个函数后面,该函数不会被执行
buttonOneEle.addEventListener("click",(event) => {
console.log(`${event.type}: ${event.target.id}`);
}, false);
// buttonOne 元素的click事件没有向上冒泡,该函数不会被执行
Array.from(document.querySelectorAll('.item')).forEach(item => {
item.addEventListener('click',function(event){
console.log(`${event.type}: ${event.target.id}`)
}, false);
})
在浏览器打开Demo,点击buttonOne
元素或者其他元素时,在console
中输出的结果像下面这样:
阻止默认行为
preventDefault
它是事件对象(Event
)的一个方法,作用是取消一个目标元素的默认行为。既然是默认行为,那就说元素必须要有默认行为才能被取消,如果元素自身没有默认行为,调用该方法就会无效。
下例演示了如何使用preventDefault
方法来阻止一个input
元素内非法字符的输入。
<body>
<p>请输入一些字母,只允许小写字母.</p>
<form>
<input type="text" id="my-textbox"/>
</form>
<script type="text/javascript">
function checkName(evt) {
var charCode = evt.charCode;
if (charCode != 0) {
if (charCode < 97 || charCode > 122) {
evt.preventDefault();
alert("只能输入小写字母." + "\n"
+ "charCode: " + charCode + "\n"
);
}
}
}
document.getElementById('my-textbox').addEventListener(
'keypress', checkName, false
);
</script>
</body>
另外,在监听器中调用事件对象的preventDefault
方法,可以避免使用事件取消执行默认操作。你可以查看 event.cancelable
属性来判断一个事件的默认动作是否可以被取消. 在cancelable
属性为false
的事件上调用 preventDefault
方法没有任何效果.
preventDefault
方法不会阻止该事件的进一步冒泡。 event.stopPropagation
方法才有这样的功能.
总结
在这篇文章中,我们深入的了解了JavaScript中DOM事件传播机制。JavaScript的DOM事件会经历捕获阶段、目标阶段和冒泡阶段三个阶段。通过示例介绍事件捕获和事件冒泡机制,以及如何中断事件、取消事件和阻止事件冒泡等。
扩展阅读
- Event Capturing and Bubbling in JavaScript
- Document Object Model (DOM) Level 2 Events Specification
- What Is Event Bubbling in JavaScript? Event Propagation Explained
- W3C DOM4 – Events
- DOM – Living Standard – Events
- W3C UI Events – DOM Event Architecture
- MSN – Events and the DOMadidas Dame 5 X Marvel 'Black Panther'