js事件机制:监听、捕获、冒泡与委托

众所不周知,在浏览器中,原生的 DOM 事件流包含三个阶段:捕获阶段、目标阶段、冒泡阶段。

而 DOM 事件流的执行流程,也严格按照:事件捕获 -> 事件目标 -> 事件冒泡 的顺序来执行。

本文将结合代码示例,详细讲解事件监听,事件捕获,事件冒泡和事件委托机制。

一、事件监听函数addEventListener

在JavaScript中,事件监听通过 addEventListener 来实现,这是现代浏览器推荐的标准方式(DOM2 级事件),与传统的 DOM0 级事件(如 onclick)相比,它提供了更灵活的控制能力。

1.1 监听范围判定

事件监听的传播遵从以下原则:当一个元素接收到事件的时候,它会把他接收到的事件传给自己的父级,并一层层向外传递,直到传到 window,而父元素被触发时则不会将事件传给子元素。

为了更好理解,我们以下面这两个盒子为例,其中,青色盒子是粉色盒子的子元素。

代码示例

javascript 复制代码
  document.getElementById('parent').addEventListener('click',function(event){
            console.log("我是大盒子")
        })
        
  document.getElementById('child').addEventListener('click',function(event){
            console.log("我是小盒子")
        })

此时,当我们点击小盒子,在控制台我们能看到:

而当我们点击大盒子时,运行结果为:

由此,可以验证:当子元素接收到事件的时候,它会把他接收到的事件传给自己的父级,而父元素被触发时则不会将事件传给子元素。

1.2 监听函数的参数

从上面的代码我们可以看到,一般情况下,addEventListener()默认只有两个参数,其中,一个是绑定的事件,一个是事件触发后要执行的事件处理函数,即:

arduino 复制代码
addEventListener('event','function')

然而,真的是这样吗?事实上,addEventListener()还存在隐藏的第三个参数:useCapture,这个参数的作用是决定在事件冒泡阶段要不要调用事件处理函数

arduino 复制代码
addEventListener('event','function','useCapture')

默认情况下,useCapture的值为false,这表示在事件冒泡阶段调用事件处理函数,当我们将它改为true时,那么,它将变为在事件捕获阶段调用处理函数。

那么,这有什么区别呢?我们来看个例子:

我是例子

javascript 复制代码
  document.getElementById('parent').addEventListener('click',function(event){
            console.log("我是大盒子")
        },false)
        
  document.getElementById('child').addEventListener('click',function(event){
            console.log("我是小盒子")
        },false)

此时,我们将第三个参数useCapture设置为false(也就是默认值),这时允许事件冒泡,我们点击小盒子,在可以看到:

此时的打印结果为小盒子->大盒子,这个打印结果完全符合事件冒泡阶段事件从内而外的处理顺序,而相同的代码,我们将false改为true后,打印结果为:

此时输出顺序变为了大盒子->小盒子,也符合阻止事件冒泡后的事件执行顺序。


二、事件捕获与事件冒泡

事件捕获 :它是 DOM 事件流的第一个阶段,当鼠标点击目标事件后,事件将会从最外层的 windowdocument 开始,自外而内地传播到目标元素,其触发时机要早于目标元素的事件处理。

事件冒泡 :它是事件流的最后一个阶段,它和事件捕获相似,但是事件传播方向相反,它的事件是从目标元素开始,自内而外 地传播到根节点(如 documentwindow)。

还是以上面的代码为例,不过这次我们进行对照测试:

javascript 复制代码
//阻止事件冒泡组
document.getElementById('parent').addEventListener('click',function(event){
            console.log("大盒子---向内")
        },true)
        
document.getElementById('child').addEventListener('click',function(event){
            console.log("小盒子---向内")
        },true)

//允许事件冒泡组
document.getElementById('child').addEventListener('click',function(event){
            console.log("小盒子---向外")
        },false)
        
document.getElementById('parent').addEventListener('click',function(event){
            console.log("大盒子---向外")
        },false)
        

由上面地结果我们可以得知:事件捕获与事件冒泡是比较相似的,区别在于事件传播的方向不同。


四、事件委托

4.1 概念介绍

事件委托 :事件委托就是利用冒泡机制,将子元素的事件处理委托给父元素进行处理 ,因为依赖冒泡机制,所以,如果阻断了事件冒泡,事件委托也无法实现。

事件委托的作用:

先想象一下,我们有一个列表,里面有10000项数据,那么,我想要监听每一个数据的变化,按照正常的想法,我们是不是应该使用 for 循环或其他方式,分别给这10000个元素都添加一个监听事件?

然而,事件委托混合地解决了这个问题。

从上面的事件捕获和事件冒泡我们已经得知,当子元素触发事件时,会将事件传递给父元素,那么,根据这个原理,我们就可以只在父元素上添加一个监听事件,通过这种方法,达到监听所有子元素的目的。

4.2 优缺点分析

优点

1.内存方面

  • 事件委托只需在父元素上绑定一个事件监听器,而不是为每个子元素单独绑定,这极大减少内存的消耗。

  • 事件委托适合动态列表、表格等大量子元素的场景

2.代码方面

  • 新增的子元素无需重新绑定事件,天然支持动态 DOM 结构,如: // 点击新增的按钮依然能触发事件 document.getElementById('list').addEventListener('click', (e) => { if (e.target.matches('button')) { console.log('按钮被点击'); } });

  • 事件委托避免循环绑定事件,减少重复代码。

  • 有利统一管理事件逻辑,降低维护成本。

3.性能方面

  • 减少浏览器事件监听器的数量,提升页面响应速度(尤其对移动端更友好)

缺点

1.事件目标判断复杂

  • 比如需要通过 event.targetevent.currentTarget 精确识别触发元素,嵌套结构时可能需配合 closest() 方法。

    less 复制代码
    // 如果子元素内有嵌套的 span,需额外判断
    if (e.target.closest('.btn')) { ... }

2.不适合所有事件类型

  • 部分事件不冒泡 (如 focusblurload 等),无法使用事件委托。
  • 解决方案:用冒泡替代事件(如 focusin 代替 focus)。

3.事件阻止需谨慎

  • 如果在委托的父元素上调用 stopPropagation(),会影响其他子元素的正常事件流

相关推荐
中微子4 分钟前
闭包面试宝典:高频考点与实战解析
前端·javascript
G等你下课32 分钟前
告别刷新就丢数据!localStorage 全面指南
前端·javascript
爱编程的喵36 分钟前
JavaScript闭包实战:从类封装到防抖函数的深度解析
前端·javascript
前端Hardy41 分钟前
8个你必须掌握的「Vue」实用技巧
前端·javascript·vue.js
星月日1 小时前
深拷贝还在用lodash吗?来试试原装的structuredClone()吧!
前端·javascript
爱学习的茄子1 小时前
JavaScript闭包实战:解析节流函数的精妙实现 🚀
前端·javascript·面试
今夜星辉灿烂1 小时前
nestjs微服务-系列4
javascript·后端
吉吉安1 小时前
两张图片对比clip功能
javascript·css·css3
布兰妮甜1 小时前
开发在线商店:基于Vue2+ElementUI的电商平台前端实践
前端·javascript·elementui·vue
Jinxiansen02111 小时前
Vue 3 中父子组件双向绑定的 4 种方式
javascript·vue.js·ecmascript