JS事件机制:从基础到进阶

186 阅读4分钟

前言

今天我们来聊一聊JS 的事件机制:众所周知,JavaScript 是一个事件驱动的语言,当浏览器载入网页开始读取后,虽然马上会读取JavaScript 事件相关的代码,但是必须要等到「事件」被触发(如使用者点击、按下键盘等)后,才会再进行对应代码段的执行。换句话说,html里面的DOM元素一直在等待事件的触发。

举个例子:电话如果没有人打电话过来,我们是不会动的,当别人打电话过来的时候,我们才会去接那个电话。电话响了(事件被触发) -> 接电话(去做对应的事)

接下来让我们来探讨一下DOM事件级别

DOM事件

DOM有4次版本更新,与DOM版本变更,产生了3种不同的DOM事件 :DOM 0级事件处理,DOM 2级事件处理和DOM 3级事件处理。由于DOM 1级中没有事件的相关内容,所以没有DOM 1级事件。

DOM0级事件

<input onclick="alert('hello')"/>

onclick(鼠标点击)就是一个DOM0级事件,但是现在已经不提倡用了,因为html和js杂糅在一起,耦合度太高,不利于维护,因此现在已经不建议用此方式来绑定事件。

DOM2级事件

DOM2级事件模型是对DOM0级事件的重大改进,提供了更强大、更灵活的事件处理机制,是现代Web开发中事件处理的标准方式。Dom 2级事件是通过 addEventListener 绑定的事件。DOM2级事件支持事件捕获和事件冒泡(捕获,目标,冒泡)同时更精确的事件控制(可以指定在捕获阶段还是冒泡阶段处理)。

Dom 2级事件有三个参数:第一个参数是事件名(如click);第二个参数是事件处理程序函数;第三个参数如果是true的话表示在捕获阶段调用,为false的话表示在冒泡阶段调用。捕获阶段和冒泡阶段在下一节具体介绍。

DOM事件流

事件流(Event Flow)指的就是网页接收事件的顺序,事件流有两种机制:

  • 事件捕获(Event Capturing)
  • 事件冒泡(Event Bubbling)

当一个事件发生后,会在子元素和父元素之间传播(propagation)。这种传播分成三个阶段:

  1. 捕获阶段:事件从window对象自上而下向目标节点传播的阶段;
  2. 目标阶段:真正的目标节点正在处理事件的阶段;
  3. 冒泡阶段:事件从目标节点自下而上向window对象传播的阶段。

接下来我们来看一下事件捕获和事件冒泡。

事件捕获

image.png

事件捕获是从根节点(通常是window 或者 document 开始),逐级向下遍历 DOM 树,直到到达触发事件的目标元素(target

事件冒泡

image.png

事件冒泡是从目标元素(target)开始,逐级向上传播到根节点(window)。

接下来让我们来看一个例子,它会让你更加深入的了解事件捕获和事件冒泡

<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <title>JS DOM2 事件机制</title>
    <style>
        #parent {
           width: 200px;
           height: 200px;
            background-color: blue;
        }
        #child {
            width: 100px;
            height: 100px;
            background-color: red;
        }
    </style>
</head>
<body>
    <div id="parent"  >
        <div id="child">

        </div>
    </div>
    <script>
    document.getElementById('parent')
    .addEventListener('click',function(event){
        console.log('父元素clicked')
    },false)
    document.getElementById('child')
    .addEventListener('click',function(event){
        console.log('子元素clicked')
    },true)
    </script>
</body>
</html>

让我们来一步步分析一下:

  1. 1
document.getElementById('parent') .addEventListener('click',function(event){ console.log('父元素clicked') },false)

parent节点注册了一个点击事件监听的事件,useCapture 的值为false(也就是默认值),也就是在冒泡阶段触发监听。

  1. 2
document.getElementById('child') .addEventListener('click',function(event){ console.log('子元素clicked') },true)

child节点注册了一个点击事件监听的事件,useCapture 的值为true,也就是在捕获阶段触发

  1. 3 事件流的顺序

  2. 捕获阶段(Capture Phase)

    • 事件从 document 向下传播到 #child
    • 由于 #child 设置了 useCapture: true,它的监听器会触发。
    • 输出:子元素clicked
  3. 目标阶段(Target Phase)

    • 事件到达 #child(目标元素),但这里没有额外的监听器。
  4. 冒泡阶段(Bubble Phase)

    • 事件从 #child 向上冒泡到 #parent
    • 由于 #parent 设置了 useCapture: false(默认冒泡阶段),它的监听器会触发。
    • 输出:父元素clicked

当我们点击子元素的时候:

image.png

总结

在实际开发中,我们可以根据具体场景选择合适的事件处理方式。例如,对于静态元素较多的页面,可以使用传统的事件绑定方式;对于动态元素较多的页面,使用事件委托可以提高性能。同时,在 React 项目中,要充分利用合成事件的特性,避免不必要的性能开销。