众所不周知,在浏览器中,原生的 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 事件流的第一个阶段,当鼠标点击目标事件后,事件将会从最外层的 window
或 document
开始,自外而内地传播到目标元素,其触发时机要早于目标元素的事件处理。
事件冒泡 :它是事件流的最后一个阶段,它和事件捕获相似,但是事件传播方向相反,它的事件是从目标元素开始,自内而外 地传播到根节点(如 document
或 window
)。
还是以上面的代码为例,不过这次我们进行对照测试:
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.target
或event.currentTarget
精确识别触发元素,嵌套结构时可能需配合closest()
方法。less// 如果子元素内有嵌套的 span,需额外判断 if (e.target.closest('.btn')) { ... }
2.不适合所有事件类型
- 部分事件不冒泡 (如
focus
、blur
、load
等),无法使用事件委托。 - 解决方案:用冒泡替代事件(如
focusin
代替focus
)。
3.事件阻止需谨慎
- 如果在委托的父元素上调用
stopPropagation()
,会影响其他子元素的正常事件流。