Skip to content

在浏览器环境中,JS 大多数的应用场景是在做一些交互效果以提升用户体验,什么是交互效果呢?就是用户做了一些操作,我们就根据用户的操作干一些事。这里就存在一个问题,我们已经知道如何用 JS 干一些事,比如弹窗,比如修改某个元素的样式,添加某个元素等等,但我们应该怎么知道用户做了什么操作呢?这就事件干的事。

实际上,事件的概念非常简单,发生一件事其实就是事件,比如点击事件,就是用户点击了,比如鼠标移入事件,就是鼠标移入到了某个元素里面,这些都是事件,很简单吧。事件其实有很多类型,所谓的事件类型实际上就是发生了什么事,也就是鼠标移动是一个事件类型,鼠标按下是一个事件类型,鼠标点击也是一个事件类型,包括什么键盘按下和抬起,这些都是事件类型。当某件事发生的时候,应该执行一些功能,这个功能实际上就是时间处理程序,这个事件处理程序以函数的形式给出。

说白了,事件就是某个元素发生了某件事请之后运行某个函数,这里有三个名词,某个元素,某个事情,某个函数,为了实现这个功能,我需要提前把这三个东西绑定在一起吧,这个绑定在一起的过程,就是事件注册。以前面提到的 click 事件为例

js
dom.onclick = function(){
	console.log("test");
}

可见,dom 就是这个元素,click 就是事件类型,function(){console.log("test")} 就是事件处理程序。这里就是把这三者绑定在一起了,所以这个过程就是事件注册。当完成了这个事件注册之后,如果用户点击了这个 dom,那么就会自动执行这个事件处理函数。

这里其实有一个问题,看上面的事件注册,当我点击 dom 的时候,dom 的父元素是不是也可以看做被点击了?所以实际上,当我们给 dom 进行了事件注册的时候,不仅 dom 会实时监听这样一个点击事件,dom 的父元素也可以监听到,包括父元素的父元素也同理,所以如果父元素也绑定一个事件处理函数, 当点击 dom 的时候,这个事件处理函数一样会被触发。那这里就有一个先后的问题,这些元素发生事件的顺序,我们也称为事件流。

js
//dom:元素
//domFather:元素的父元素
dom.onclick = function(){
	console.log("dom 被点击了");
}
domFather.onclick = function(){
	console.log("dom的父元素被点击了");
}

显然,在这个例子中,当 dom 被点击的时候,由于 dom 包含在 domFather 内部,所以如果视角放到 domFather 上,也可以认为 domFather 被点击了,所以当我们点击 dom 的时候,这两个函数实际上都会被触发,这里涉及到触发顺序的问题,这里的触发顺序实际上就是事件流。

实际上,哪怕我不给 dom 注册点击事件,当我点击 dom 的时候,dom 也会监听到这个点击事件,只不过由于我没有给他注册事件,或者说没有给他绑定事件处理函数,所以当他监听到这个事件的时候他无事可做。所以我们看起来他毫无效果。

总之,如果一个元素发生了该事件,那么由于该元素本身处于其所有祖先元素内部,所以该元素的所有祖先元素都应该发生该事件。

我们可以写这样一个例子来验证

html
    <style>
            * {
                margin: 0;
                padding: 0;
            }
    
            #grandFather {
                width: 200px;
                height: 200px;
                background-color: red;
            }
    
            #Father {
                width: 100px;
                height: 100px;
                background-color: green;
            }
    
            #Son {
                width: 50px;
                height: 50px;
                background-color: blue;
            }
        </style>
    <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.onclick = function () {
            console.log("Son被点击了");
        }
        Father.onclick = function () {
            console.log("Father被点击了");
        }
        grandFather.onclick = function () {
            console.log("grandFather被点击了");
        }
    </script>

recording

可见,当我点击最内部的 Son 元素时,不仅触发了 Son 元素的点击事件,它祖先元素的点击事件也会被触发,这就是我要提到的东西,当一个元素的某个事件被触发的时候,其祖先元素也会监听到这个事件。

这个祖先元素是基于 HTML 结构的,而不是视觉结构,例如我可以使用定位让 Son 元素从视觉上挪到其祖先元素外面去,但并不影响其祖先元素还是其祖先元素的事实,他的祖先元素还是可以监听到这个事件。

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

可见,此时虽然从视觉上来看,蓝色的 Son 元素在其父元素的外面,但在 HTML 结构中,Father 和 grandFather 元素还是 Son 元素的祖先元素,所以也能监听到 Son 元素的点击事件。

不知道读者是否注意到这样一个细节,对于这两次点击事件,其执行顺序都是 Son -> Father -> grandFather,这种由内向外的触发顺序,我们称为事件冒泡,也就是这个事件的触发顺序是先触发最内层的事件,再触发其父元素的事件,这样依次向外触发。但是浏览器也支持从外向内的触发顺序,即先触发最外层的事件,再依次向内触发,直到触发被点击元素的事件,这种由外向内的事件触发顺序我们称为事件捕获。

默认情况下,浏览器以事件冒泡的形式触发,所以默认情况下,事件的触发顺序是从内向外的,但这个是可以修改的,修改方式参见后面的内容。

事件流

我们从这张图可以看到事件的触发流程,以这张图为例,事件的触发顺序应该是下面这个顺序

  • document 的事件捕获阶段
  • html 的事件捕获阶段
  • body 的事件捕获阶段
  • div 的事件捕获阶段
  • button 事件捕获阶段
  • button 事件冒泡阶段
  • div 的事件冒泡阶段
  • body 的事件冒泡阶段
  • html 的事件冒泡阶段
  • document 的事件冒泡阶段

任何一个事件的处理顺序都是这样,都是先从外向内触发一次捕获阶段的事件,再由内向外触发一次事件冒泡阶段的事件,默认情况下,我们注册的事件都是事件冒泡阶段的处理函数,所以事件的执行顺序才会是由内向外的,在后面我们学习了如何给事件捕获阶段添加处理函数之后,我们就可以在事件捕获阶段做一些处理。

我们看到,一个事件触发之后,该元素和其祖先元素上的事件其实是被触发了两次的,捕获阶段会触发一次,冒泡阶段还会触发一次,我们默认情况下注册的是冒泡阶段的处理函数,没有学习如何在事件捕获阶段注册事件,所以目前在事件捕获阶段其实并不会做任何处理,但读者应该要清楚这个事件被触发了两次,捕获阶段没有触发事件只是因为我们没有注册事件处理函数。

综上,一个事件的处理可以分为三个部分,先是由外向内依次触发一次捕获阶段的事件,再触发元素自身的事件,然后再由内向外触发一次冒泡阶段的事件。分别对应了捕获阶段、目标阶段、冒泡阶段。事件目标阶段只会存在一个 DOM,如上图中的 button,如前面代码中的 Son 元素,事件目标阶段处理的元素,我们称为事件源(事件目标)。

实际上,事件源非常好理解,当我们触发一个元素的事件后,它并不是直接触发这个元素的事件,而是先从外到内依次触发其祖先元素的捕获阶段处理函数,然后再执行这个元素本身的处理函数,然后再由内向外触发一次处理函数。而最里面这个元素,也就是我们触发事件的这个元素,就是事件源。

以笔记为镜,可以知得失