上一章JavaScript教程请查看:JS事件监听器
在本教程中,你将了解事件如何在JavaScript中的DOM树中传播。
理解事件传播
事件传播是一种机制,它定义了事件如何传播或通过DOM树到达目标,以及之后会发生什么。
让我们通过一个示例来理解这一点,假设你为嵌套在一个段落内的超链接(即<a>元素)分配了一个单击事件处理程序(即<p>元素)。现在,如果单击该链接,将执行处理程序。但是,如果你将click事件处理程序分配给包含链接的段落,而不是link,那么即使在这种情况下,单击链接仍然会触发处理程序。这是因为事件不仅影响生成事件的目标元素——它们在DOM树中上下移动以到达目标。这称为事件传播
在现代浏览器中,事件传播分为两个阶段:捕获阶段(capturing)和冒泡阶段(bubbling)。在我们进一步讨论之前,先来看看下面的例子:
上面的图像演示了事件在具有父元素的元素上触发事件时,在事件传播的不同阶段,事件如何在DOM树中传播。
引入事件传播的概念是为了处理这样一种情况,即具有父子关系的DOM层次结构中的多个元素具有针对同一事件的事件处理程序,例如鼠标单击。现在的问题是,当用户单击内部元素时,将首先处理哪个元素的单击事件—外部元素的单击事件,还是内部元素。
在本章接下来的章节中,我们将更详细地讨论事件传播的各个阶段,并找出这个问题的答案。
注:形式上分为3个阶段:捕获阶段、目标阶段和冒泡阶段。但是,第二阶段,即目标阶段(当事件到达生成事件的目标元素时发生)在现代浏览器中没有单独处理,为捕获和冒泡阶段注册的处理程序在此阶段执行。
捕获阶段
在捕获阶段,事件从窗口向下通过DOM树传播到目标节点。例如,如果用户单击超链接,则单击事件将通过<html>元素、<body>元素和包含链接的<p>元素。
此外,如果目标元素的任何祖先(即父、祖父等)和目标本身具有针对该类型事件的专门注册的捕获事件监听器,则这些监听器将在此阶段执行。让我们看看下面的例子:
<!DOCTYPE html>
<head>
<meta charset="utf-8">
<title>Event捕获Demo</title>
<style type="text/css">
div, p, a{
padding: 15px 30px;
display: block;
border: 2px solid #000;
background: #fff;
}
</style>
</head>
<body>
<div id="wrap">DIV
<p class="hint">P
<a href="#">A</a>
</p>
</div>
<script>
function showTagName() {
alert("Capturing: "+ this.tagName);
}
var elems = document.querySelectorAll("div, p, a");
for(let elem of elems) {
elem.addEventListener("click", showTagName, true);
}
</script>
</body>
</html>
事件捕获在所有浏览器中都不受支持,并且很少使用。例如,版本9.0之前的Internet Explorer不支持事件捕获。
另外,当第三个参数设置为true时,事件捕获仅对用addEventListener()方法注册的事件处理程序有效。传统的分配事件处理程序的方法,如使用onclick、onmouseover等,在这里不起作用。请查看JavaScript事件监听器一章以了解更多关于事件监听器的信息。
冒泡阶段
在冒泡阶段,情况正好相反。在此阶段中,事件从目标元素向上传播或冒泡到窗口,一个接一个地访问目标元素的所有祖先。例如,如果用户单击超链接,则单击事件将通过包含链接的<p>元素、<body>元素、<html>元素和文档节点。
另外,如果目标元素和目标本身的任何祖先都为该类型的事件分配了事件处理程序,那么这些处理程序将在此阶段执行。在现代浏览器中,默认情况下,所有事件处理程序都注册在冒泡阶段。让我们来看一个例子:
<!DOCTYPE html>
<head>
<meta charset="utf-8">
<title>Event冒泡Demo</title>
<style type="text/css">
div, p, a{
padding: 15px 30px;
display: block;
border: 2px solid #000;
background: #fff;
}
</style>
</head>
<body>
<div onclick="alert('Bubbling: ' + this.tagName)">DIV
<p onclick="alert('Bubbling: ' + this.tagName)">P
<a href="#" onclick="alert('Bubbling: ' + this.tagName)">A</a>
</p>
</div>
</body>
</html>
所有浏览器都支持事件冒泡,它适用于所有处理程序,不管它们是如何注册的,例如使用onclick或addEventListener()(除非它们注册为捕获事件监听器)。这就是为什么事件传播这个术语经常被用作事件冒泡的同义词。
访问目标元素
目标元素是生成事件的DOM节点。例如,如果用户单击超链接,目标元素就是超链接。
目标元素event.target可以作为事件访问,它不会在事件传播阶段改变。此外,this关键字表示当前元素(即具有当前正在运行的处理程序的元素)。让我们来看一个例子:
<!DOCTYPE html>
<head>
<meta charset="utf-8">
<title>Event目标Demo</title>
<style type="text/css">
div, p, a{
padding: 15px 30px;
display: block;
border: 2px solid #000;
background: #fff;
}
</style>
</head>
<body>
<div id="wrap">DIV
<p class="hint">P
<a href="#">A</a>
</p>
</div>
<script>
// 选择div元素
var div = document.getElementById("wrap");
// 附加onclick事件处理程序
div.onclick = function(event) {
event.target.style.backgroundColor = "lightblue";
// 让浏览器在显示警告之前完成背景颜色的渲染
setTimeout(() => {
alert("target = " + event.target.tagName + ", this = " + this.tagName);
event.target.style.backgroundColor = ''
}, 0);
}
</script>
</body>
</html>
我们在上面的例子中使用的胖箭头(=>)符号是一个箭头函数表达式。它的语法比函数表达式更短,而且可以使this关键字正常工作。请查看关于ES6特性的教程以了解更多关于箭头函数的信息。
停止事件传播
如果希望防止任何祖先元素的事件处理程序得到关于事件的通知,还可以在中间停止事件传播。
例如,假设你有嵌套的元素,并且每个元素都有显示警告对话框的onclick事件处理程序。通常,当你单击内部元素时,所有处理程序都将同时执行,因为事件会出现在DOM树中。
<!DOCTYPE html>
<head>
<meta charset="utf-8">
<title>事件传播Demo</title>
<style type="text/css">
div, p, a{
padding: 15px 30px;
display: block;
border: 2px solid #000;
background: #fff;
}
</style>
</head>
<body>
<div id="wrap">DIV
<p class="hint">P
<a href="#">A</a>
</p>
</div>
<script>
function showAlert() {
alert("点击了: "+ this.tagName);
}
var elems = document.querySelectorAll("div, p, a");
for(let elem of elems) {
elem.addEventListener("click", showAlert);
}
</script>
</body>
</html>
为了防止这种情况,可以使用event. stoppropagation()方法阻止事件在DOM树中冒泡。在下面的示例中,如果单击子元素,则父元素上的单击事件侦听器将不执行。
<!DOCTYPE html>
<head>
<meta charset="utf-8">
<title>停止事件传播Demo</title>
<style type="text/css">
div, p, a{
padding: 15px 30px;
display: block;
border: 2px solid #000;
background: #fff;
}
</style>
</head>
<body>
<div id="wrap">DIV
<p class="hint">P
<a href="#">A</a>
</p>
</div>
<script>
function showAlert(event) {
alert("点击了: "+ this.tagName);
event.stopPropagation();
}
var elems = document.querySelectorAll("div, p, a");
for(let elem of elems) {
elem.addEventListener("click", showAlert);
}
</script>
</body>
</html>
此外,你甚至可以使用stopImmediatePropagation()方法来防止附加到相同事件类型的相同元素的任何其他侦听器被执行。
在下面的示例中,我们将多个侦听器附加到超级链接,但是当你单击链接时,只有一个超级链接侦听器将执行,并且你将只看到一个警报。
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="utf-8">
<title>立即停止传播Demo</title>
<style type="text/css">
div, p, a{
padding: 15px 30px;
display: block;
border: 2px solid #000;
background: #fff;
}
</style>
</head>
<body>
<div onclick="alert('点击了: ' + this.tagName)">DIV
<p onclick="alert('你点击了: ' + this.tagName)">P
<a href="#" id="link">A</a>
</p>
</div>
<script>
function sayHi() {
alert("Hi, there!");
event.stopImmediatePropagation();
}
function sayHello() {
alert("Hello World!");
}
// 将多个事件处理程序附加到超链接
var link = document.getElementById("link");
link.addEventListener("click", sayHi);
link.addEventListener("click", sayHello);
</script>
</body>
</html>
注意:如果为相同的事件类型将多个侦听器附加到相同的元素,则将按照它们被添加的顺序执行它们。但是,如果任何侦听器调用event.stopImmediatePropagation()方法,则不会执行任何其他侦听器。
防止默认操作
有些事件具有与之关联的默认操作。例如,如果你点击一个链接浏览器带你到链接的目标,当你点击一个表单提交按钮浏览器提交表单,等等。可以使用事件对象的preventDefault()方法来防止此类默认操作。
但是,阻止默认操作并不会停止事件传播;事件像往常一样继续传播到DOM树。下面是一个例子:
<!DOCTYPE html>
<head>
<meta charset="utf-8">
<title>防止默认操作Demo</title>
</head>
<body>
<form action="/examples/html/action.php" method="post" id="users">
<label>First Name:</label>
<input type="text" name="first-name" id="firstName">
<input type="submit" value="Submit" id="submitBtn">
</form>
<script>
var btn = document.getElementById("submitBtn");
btn.addEventListener("click", function(event) {
var name = document.getElementById("firstName").value;
alert("Sorry, " + name + ". The preventDefault() 不能让你提交这份表单!");
event.preventDefault(); // 阻止表单提交
});
</script>
</body>
</html>
评论前必须登录!
注册