JavaScript 事件处理机制(事件模型)
DOM是树形结构,如果父子节点都绑定事件,事件处理先后的顺序如何决定?
这就涉及到事件流的概念:
一、事件流
事件流指的是从页面中接受事件的顺序。
事件冒泡
事件冒泡是一种从下往上的传播方式,事件由文档中嵌套层次最深的节点逐渐传播到高层父节点。
事件捕获
事件捕获流处理事件的顺序与事件冒泡相反。
JavaScript事件处理程序遵循先捕获(父节点先发现事件)后冒泡(从子节点向上传递)的规则。 如下图所示:
二、DOM0事件模型
以鼠标点击事件为例,以下代码在两个div上分别定义了鼠标点击事件,这种事件的定义方式为DOM0事件:
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>JavaScript事件流</title>
<style>
#out{
width: 200px;
height: 200px;
background-color: blue;
}
#in{
width: 100px;
height: 100px;
background-color: blueviolet;
}
</style>
</head>
<body>
<div id="out">
<div id="in">
</div>
</div>
<script>
var outer = document.getElementById("out");
var inner = document.getElementById("in");
outer.onclick = function () {
console.log("点击了外部div");
};
inner.onclick = function () {
console.log("点击了内部div");
}
</script>
</body>
</html>
当点击内部div时,控制台输出的顺序为:
由此可见,对于DOM0级事件,浏览器是在冒泡阶段对触发的事件进行响应的。
此外对于DOM0级事件来说,同一元素的某一事件只能注册一个,后边注册的同种事件会覆盖之前注册的事件。正是因为这个特点,要想解除某一元素注册的DOM0级事件,只需要再将其事件注册为null即可。例如,要想解除以上代码中外部div的鼠标点击事件,只需要在后面加上一句以下代码即可:
outer.onclick = null;
三、DOM2事件模型
修改以上注册事件的方式即可定义DOM2级事件:
var outer = document.getElementById("out");
var inner = document.getElementById("in");
outer.addEventListener("click", function (e) {
console.log("点击了外部div(DOM2级事件)");
}, false);
outer.addEventListener("click", function (e) {
console.log("点击了外部div(DOM2级事件——第二个点击事件)");
}, false);
inner.addEventListener("click", function (e) {
console.log("点击了内部div(DOM2级事件)");
}, false);
通过addEventListener方法注册的事件称为DOM2级事件,该方法一共接收3个参数:
- 第一个参数:注册的事件类型(注意前面没有on)
- 第二个参数:回调函数,即事件响应方法
- 第三个参数:true/false,代表是否捕获,若设置为true,代表在事件捕获阶段触发事件;若设置为false,代表在事件冒泡阶段触发事件。一般情况下该参数均设置为false。
在修改了事件模型之后,点击内部div时控制台显示的消息为:
因为设置了事件冒泡,所以浏览器按照事件冒泡的顺序处理事件:先触发内部div点击事件,再触发外部div点击事件。可以看到,不同于DOM0级事件,DOM2级事件允许为同一元素注册多个同类事件。
当将以上addEventListener函数的第三个参数均改为true(启用捕获)时,控制台输出的消息顺序变为:
由于父节点先被捕获,所以首先触发了外部div的事件。
在DOM2级事件中,由于后注册的事件不会覆盖之前注册的事件,所以不能通过将元素事件注册为null的方式来解除事件。在DOM2级事件模型中,要想解除事件,需要调用removeEventListener函数,该函数的参数与addEventListener方法的参数完全一致,用于解除注册的DOM2级事件。这里需要注意的是,要想解除某一事件,必须把回调函数先保存起来,匿名函数无法移除。
阻止冒泡
有的时候,当点击了内部div后,我们只希望触发内部div的点击方法,不希望触发外部div的点击方法,这时候就要用到阻止冒泡。
阻止冒泡的方法很简单,只要在内部div的点击方法中添加一行代码就可以实现阻止冒泡:
inner.addEventListener("click", function (e) {
console.log("点击了内部div(DOM2级事件)");
e.stopPropagation(); // 阻止冒泡
}, false);
阻止冒泡后,点击内部div时,控制台将不再输出外部div的点击消息: