Skip to content

事件注册也可以叫做事件绑定,我们知道,当一个 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>

![recording 2](/notes-assets/02.JavaScript/7.DOM事件/assets/recording 2.gif)

当我们点击的时候,运行顺序如下所示:

  • grandFather 捕获阶段处理函数
  • Father 捕获阶段处理函数
  • Son 捕获阶段处理函数
  • Son 冒泡阶段处理函数
  • Father 冒泡阶段处理函数
  • grandFather 冒泡阶段处理函数

我们再次画图详细解释这个事件的执行过程。

![Pasted image 20240412025927](/notes-assets/02.JavaScript/7.DOM事件/assets/Pasted image 20240412025927.png)

我们只看这个局部过程,首先,事件触发的时候,会从外向内依次执行每个 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

以笔记为镜,可以知得失