事件注册也可以叫做事件绑定,我们知道,当一个 DOM 元素的某个事件被触发的时候,是先从外向内执行一次事件捕获阶段的处理函数,再执行这个元素的事件处理函数,然后再由内向外执行一次事件冒泡阶段的处理函数。所谓的事件注册,无非就是绑定一些函数。
事件注册主要有两种方式,我们前面见过的一种注册方式就是,在事件名称前面加上 on,作为这个 dom 元素的一个属性名,我们将这个属性赋值为一个函数,就完成了事件注册,这种方式我们已经见过了,即前面的将 onclick 属性赋值为一个函数,由于默认情况下是注册的冒泡阶段的处理函数,所以相当于在冒泡阶段就会执行这个函数。
实际上,直接把事件写在 HTML 的属性上也是可以的,但是非常少见
html
<button onclick="console.log('被点了');"> 点击 </button>这种方式其实和上面的方式一样,这种方式的原理非常简单,本质上都是把事件处理函数作为 DOM 元素的一个属性,所以如果我们要取消这种事件,直接重新赋值为 null 或者 undefined 即可。
但是这种方式存在很多局限,例如只能绑定一次,如果我要绑定两次事件,后面绑定的会覆盖掉前面绑定的事件,就像变量的直接赋值会直接覆盖掉一样,并且,给 DOM 元素和事件相关的属性赋值,注册的只能是冒泡阶段的事件。
正是由于这种种问题,我们更推荐使用另外的一种方式来注册事件,即 dom.addEventListener() 方法来注册事件,该方法可以为一个元素注册多个事件,并不会产生前面那种后面覆盖前面的问题,也就是为一个事件添加多个处理程序。同时,这种方式也可以在捕获阶段添加处理函数。
简明语法:dom.addEventListener("事件类型",事件处理函数);
js
function handleClick1(){
console.log("handleClick1");
}
function handleClick2(){
console.log("handleClick2");
}
var btn = document.getElementById("btn");
btn.addEventListener("click",handleClick1);
btn.addEventListener("click",handleClick2);此时点击的时候,就会依次执行 handleClick1() 和 handleClick2() 两个函数,即虽然绑定了两个事件处理函数,但是并不会出现覆盖的情况,而是添加。
如果添加了多个函数,那么当事件触发的时候,会依次处理这些函数,顺序和注册顺序一致。
当然,我们也可以将函数替换为一个匿名函数,如下所示
js
btn.addEventListener("click",function (){
console.log("test");
});我们可以给该函数传入第三个参数,改参数为布尔类型,如果布尔类型为真,那么当前事件处理函数会绑定在事件捕获阶段。
html
<div id="grandFather">
<div id="Father">
<div id="Son"></div>
</div>
</div>
<script>
var grandFather = document.getElementById("grandFather");
var Father = document.getElementById("Father");
var Son = document.getElementById("Son");
Son.addEventListener("click", function () {
console.log("Son被点击了", "冒泡阶段");
});
Father.addEventListener("click", function () {
console.log("Father被点击了", "冒泡阶段");
});
grandFather.addEventListener("click", function () {
console.log("grandFather被点击了", "冒泡阶段");
});
Son.addEventListener("click", function () {
console.log("Son被点击了", "捕获阶段");
}, true);
Father.addEventListener("click", function () {
console.log("Father被点击了", "捕获阶段");
}, true);
grandFather.addEventListener("click", function () {
console.log("grandFather被点击了", "捕获阶段");
}, true);
</script>
当我们点击的时候,运行顺序如下所示:
grandFather捕获阶段处理函数Father捕获阶段处理函数Son捕获阶段处理函数Son冒泡阶段处理函数Father冒泡阶段处理函数grandFather冒泡阶段处理函数
我们再次画图详细解释这个事件的执行过程。

我们只看这个局部过程,首先,事件触发的时候,会从外向内依次执行每个 DOM 元素的事件捕获阶段的处理函数,所以会依次执行 grandFather,Father,Son 捕获阶段的事件处理函数,然后再从内向外执行时间冒泡阶段的处理函数,所以会按照 Son, Father, grandFather 的顺序执行冒泡的处理函数。
在一些教程里可能会提到这样一个概念:"对于事件源来说并不存在捕获阶段和冒泡阶段",这些教材认为哪怕给事件源注册了捕获阶段和冒泡阶段的事件,其执行顺序也和绑定顺序一致,而非先捕获再冒泡,但在我实际测试(chromium 124)的过程中发现并不是这样,哪怕是事件源的事件,不管注册顺序如何,也是先捕获再冒泡。即那些教程的说法可能只适用于多年前的浏览器。
如果使用的是 addEventListener 绑定的事件处理函数,可以使用 removeEventListener 函数来移除,基本语法为 dom.removeEventListener("事件名",函数名),该方法可以移除 dom 元素中某个事件的某个函数。
js
var Son = document.getElementById("Son");
function handleClick1() {
console.log("handleClick1");
}
function handleClick2() {
console.log("handleClick2");
}
Son.addEventListener("click", handleClick1);
Son.addEventListener("click", handleClick2);
//此时使用addEventListener给Son绑定了两个click事件,分别是handleClick1和handleClick2,当点击Son时,会依次执行handleClick1和handleClick2
Son.removeEventListener("click", handleClick1);//移除handleClick1事件
//此时移除了handleClick1事件,当点击Son时,只会执行handleClick2该函数默认移除冒泡阶段的处理函数,如果要移除捕获阶段的处理函数,需要在第三个参数传入true
很显然,removeEventListener 函数在移除事件的时候需要提供事件名,所以如果我们是以匿名函数的方式注册的事件,那么这个事件将永远也无法移除。
如果要在 IE 8 及以下注册或移除事件,需要用
attachEvent以及detachEvent方法,参数一致,并且 IE 8 及以下的版本并不支持事件捕获。
关于 `addEventListener` 和 `removeEventListener` 的第三个参数
前面提到,这两个函数的第三个参数为一个布尔值,用于设置当前对象是绑定在捕获阶段还是冒泡阶段。实际上,这两个函数不仅可以配置冒泡和捕获,还有很多配置,如果我们还要进行更多的配置,那么第三个参数应该传入一个对象,如下所示:
js
btn.addEventListener("click",function(){
console.log("test");
},{
capture:true,//事件捕获阶段
once:true,//该方法只会调用一次,调用后自动移除
});实际上,这个对象内部还可以进行两个配置,分别是 passive 属性和 signal 属性,但是我们这里不介绍,这里只强调一点,如果是注册滚轮事件或者是屏幕触摸相关事件,可以考虑将
passive属性设置为 true,可以提升性能,具体原因可以参考 EventTarget.addEventListener() - Web API 接口参考 | MDN