惯性聚合 高效追踪和阅读你感兴趣的博客、新闻、科技资讯
阅读原文 在惯性聚合中打开

推荐订阅源

T
Tailwind CSS Blog
Cyber Security Advisories - MS-ISAC
Cyber Security Advisories - MS-ISAC
雷峰网
雷峰网
量子位
有赞技术团队
有赞技术团队
阮一峰的网络日志
阮一峰的网络日志
The Cloudflare Blog
博客园 - Franky
罗磊的独立博客
宝玉的分享
宝玉的分享
博客园_首页
腾讯CDC
The GitHub Blog
The GitHub Blog
D
DataBreaches.Net
IT之家
IT之家
D
Docker
Microsoft Security Blog
Microsoft Security Blog
博客园 - 司徒正美
V
V2EX
月光博客
月光博客
N
Netflix TechBlog - Medium
爱范儿
爱范儿
I
InfoQ
P
Proofpoint News Feed

博客园 - 前端路引

高精度计算插件 decimal.js 处理 JS 浮点数精度问题(0.1 + 0.2 !== 0.3) 还在痛苦前端的拖拽排序吗?SortableJS 轻松搞定 浏览器 15 个常见指纹特征,使用插件 FingerprintJS 生成浏览器指纹 Web前端入门第 90 问:JavaScript 也能无中生有的创建音频 Web前端入门第 89 问:总结 8 种跨域通信处理方案 Web前端入门第 88 问:引入 JavaScript 的 script 标签究竟有多少用法? Web前端入门第 87 问:JavaScript 中 setInterval 和 setTimeout 细节 Web前端入门第 86 问:JavaScript 中的 Web Worker 为什么能提升代码性能? Web前端入门第 85 问:JavaScript 一个简单的 IndexedDB 数据库入门示例 Web前端入门第 84 问:JavaScript sessionStorage 那些容易踩坑的地方 Web前端入门第 83 问:JavaScript localStorage 有大小限制吗?溢出会怎样? Web前端入门第 82 问:JavaScript cookie 有大小限制吗?溢出会怎样? Web前端入门第 81 问:JavaScript cookie 的读写操作 Web前端入门第 80 问:JavaScript 哪些地方需要 try...catch 异常捕获 Web前端入门第 79 问:JavaScript async & await 的异步任务进化之路 Web前端入门第 78 问:JavaScript 比较少见的模版字符串用法 Web前端入门第 77 问:JavaScript 由程序触发绑定事件的几种方式 Web前端入门第 76 问:JavaScript 鼠标事件(mouse) enter/leave 和 over/out 区别 Web前端入门第 75 问:JavaScript 性能优化之事件委托(事件代理)原理
Web前端入门第 74 问:JavaScript 事件冒泡与事件捕获
前端路引 · 2025-07-01 · via 博客园 - 前端路引

在讨论冒泡和捕获之前,先看这么一段代码:

<style>
  .bd {
    border: 1px solid #000;
    padding: 8px;
  }
</style>

<div id="container1" class="bd">
  外层
  <div id="container2" class="bd">
    内层
    <div id="container3" class="bd">
      最内层
      <div id="container4" class="bd">
        按钮
      </div>
    </div>
  </div>
</div>

<script>
  (() => {
    const container1 = document.querySelector('#container1')
    const container2 = document.querySelector('#container2')
    const container3 = document.querySelector('#container3')
    const container4 = document.querySelector('#container4')
    container1.addEventListener('click', () => {
      console.log('container1')
    })
    container2.addEventListener('click', () => {
      console.log('container2')
    })
    container3.addEventListener('click', () => {
      console.log('container3')
    })
    container4.addEventListener('click', () => {
      console.log('container4')
    })
  })()
</script>

页面渲染大概长这样:

74-1

点击最里面的 按钮 元素,按照思维惯例,是否应该先执行 container1 的点击事件??毕竟 container1 是最外层,而且也是最先绑定事件的元素。

然而控制台输出结果为:

container4
container3
container2
container1

有点出乎意料是吧,为什么先执行的是 container4 呢?

事件冒泡

JS 绑定的事件默认是冒泡规则,什么意思呢?可以理解为:触发事件后就像水里面有一个泡泡,在水底慢慢的往上冒,从触发事件的目标元素开始,经过一层一层的盒模型,分别触发盒模型身上绑定的事件。

所以上面代码中,在点击 按钮 时,先触发了本身绑定的 click 事件,再一层一层往外传播,最终就打印出了控制台输出的结果。

事件捕获

注意:仅默认状态下,事件是冒泡规则,在绑定事件时候,可以修改第三个配置参数改为由外向内传播,这种传播顺序就是 事件捕获

以上面代码为蓝本,仅添加 addEventListener 的第三个参数为 true,就将绑定规则改为了 事件捕获 。如下:

container1.addEventListener('click', () => {
  console.log('container1')
}, true)
container2.addEventListener('click', () => {
  console.log('container2')
}, true)
container3.addEventListener('click', () => {
  console.log('container3')
}, true)
container4.addEventListener('click', () => {
  console.log('container4')
}, true)

还是点击 按钮,上面代码执行结果:

container1
container2
container3
container4

事件捕获还有另一种写法,第三个参数可以传入一个对象,通过对象的 capture 属性设置为事件捕获。

container1.addEventListener('click', () => {
  console.log('container1')
}, {
  // 另一种设置事件捕获方式
  capture: true,
})

冒泡与捕获顺序

既然同一个事件有冒泡与捕获,那么冒泡与捕获的顺序如何?上例子:

container1.addEventListener('click', () => {
  console.log('冒泡:', 'container1')
})
container2.addEventListener('click', () => {
  console.log('冒泡:', 'container2')
})
container3.addEventListener('click', () => {
  console.log('冒泡:', 'container3')
})
container4.addEventListener('click', () => {
  console.log('冒泡:', 'container4')
})
container1.addEventListener('click', () => {
  console.log('捕获:', 'container1')
}, true)
container2.addEventListener('click', () => {
  console.log('捕获:', 'container2')
}, true)
container3.addEventListener('click', () => {
  console.log('捕获:', 'container3')
}, true)
container4.addEventListener('click', () => {
  console.log('捕获:', 'container4')
}, true)

同时给元素绑定两种事件,点击 按钮 执行结果:

捕获: container1
捕获: container2
捕获: container3
捕获: container4
冒泡: container4
冒泡: container3
冒泡: container2
冒泡: container1

到这里已经可以得出结论:JS 的事件传播会经历三个阶段,由 事件捕获 开始,传递到 目标元素 之后,就改为 事件冒泡,冒泡阶段完了之后事件结束。

阻止事件传播

既然事件有传播,那程序就有办法阻止事件传播。所有事件执行时都有一个 event 对象,此对象中有方法可用于阻止事件传播。

示例:

container1.addEventListener('click', () => {
  console.log('冒泡:', 'container1')
})
container2.addEventListener('click', () => {
  console.log('冒泡:', 'container2')
})
container3.addEventListener('click', () => {
  console.log('冒泡:', 'container3')
})
container4.addEventListener('click', () => {
  console.log('冒泡:', 'container4')
})
container1.addEventListener('click', (event) => {
  event.stopPropagation()
  console.log('捕获:', 'container1')
}, true)
container2.addEventListener('click', () => {
  console.log('捕获:', 'container2')
}, true)
container3.addEventListener('click', () => {
  console.log('捕获:', 'container3')
}, true)
container4.addEventListener('click', () => {
  console.log('捕获:', 'container4')
}, true)

注意 event.stopPropagation() 这个方法,此方法是阻止事件传播的关键。

以上代码在 container1 这个元素上就阻止了事件传播,所以点击 按钮 之后,仅 container1 会执行,其他所有元素都不会再触发,结果:

捕获: container1

调用 event.stopPropagation() 就是告诉 JS,事件到此为止,不再继续了。


event 对象其他常用方法和属性:

event.target:触发事件的原始元素。
event.currentTarget:当前绑定事件的元素(等同于 this)。
event.type:事件类型(如 "click")。
event.preventDefault():阻止默认行为(如表单提交、链接跳转、自定义右键菜单)。
event.stopPropagation():阻止事件冒泡。
event.stopImmediatePropagation():阻止同一元素的其他监听器执行。
event.x 和 event.y:鼠标点击位置的坐标。


在事件中要使用 this 获取元素时,必须使用 function 函数,使用箭头函数绑定的事件 this 将会指向外层作用域的 this 指针,如下代码中箭头函数 this 指向的是 Window

<div id="container4" class="bd">
  按钮
</div>

<script>
  (() => {
    const container4 = document.querySelector('#container4')
    container4.addEventListener('click', () => {
      console.log(this) // Window 对象
    })
    container4.addEventListener('click', function () {
      console.log(this) // div#container4
    })
  })()
</script>

写在最后

编程中的细节问题,总是越挖掘越心惊,学得越来越多,才会发现知道的越来越少。