DOM事件流分为捕获、目标、冒泡三阶段:捕获从window向下至目标父级,目标阶段在点击元素上执行监听,冒泡从目标向上至window;三阶段顺序执行,共同构成完整事件传播路径。

DOM 事件流不是“点击一下就执行”,而是事件在页面结构中按固定路径走一趟——从外到内、停在目标、再从内到外。理解这趟旅程,才能真正搞懂为什么点击子元素会触发父元素的监听,以及怎么精准控制事件响应。
捕获阶段:事件从顶层往下“探路”
事件一开始并不直接落在你点的那个按钮上,而是从 window 开始,依次经过 document → html → body → 父容器 → 直到目标元素的**父级**(注意:标准规定捕获阶段不触发目标元素本身)。
这个阶段就像快递员先查楼号、再找单元门、最后站在你家门口——但还不会敲门。
- 默认不启用,需用
addEventListener('click', fn, true)显式开启 - 常用于全局拦截,比如表单提交前统一校验权限
- 老版 IE(≤8)不支持,现代浏览器都已兼容
目标阶段:事件真正“落地”
当事件到达你实际点击的元素(比如一个 <button>),就进入目标阶段。这是唯一一个必然触发该元素上所有监听函数的环节。
即使你在捕获和冒泡阶段都绑了相同事件,目标阶段也会独立执行一次——有些浏览器(如 Chrome/Firefox)甚至会在捕获阶段也触发目标元素,造成两次执行,这点要特别留意。
- 目标阶段的处理函数,无论注册在捕获还是冒泡模式下,都会运行
- 若在目标阶段调用
event.stopPropagation(),后续冒泡会立即中断 - 焦点类事件(
focus、blur)没有冒泡,所以目标阶段就是它们的“终点”
冒泡阶段:事件从目标往上传“回声”
这是最常用、也最容易被观察到的阶段。事件从目标元素开始,逐级向上通知父元素:button → div.card → div.container → body → html → document → window。
日常开发中大部分交互逻辑(如菜单展开、卡片高亮)都依赖冒泡来实现“以少控多”。
- 默认行为,
addEventListener('click', fn)第三个参数省略即为冒泡 - 阻止冒泡用
event.stopPropagation();只阻止默认行为用event.preventDefault() - 像
load、focus、scroll这类事件不冒泡,无法靠父级监听捕获
一个小实验帮你亲眼看见三阶段
准备三层嵌套 div,每层都绑定 click 监听,并分别用 true(捕获)、false(冒泡)注册:
box3.addEventListener('click', () => console.log('box3 捕获'), true)
box2.addEventListener('click', () => console.log('box2 捕获'), true)
box1.addEventListener('click', () => console.log('box1 捕获'), true)
box1.addEventListener('click', () => console.log('box1 冒泡'), false)
box2.addEventListener('click', () => console.log('box2 冒泡'), false)
box3.addEventListener('click', () => console.log('box3 冒泡'), false)
点击最内层 box1,控制台输出顺序是:
box3 捕获 → box2 捕获 → box1 捕获 → box1 冒泡 → box2 冒泡 → box3 冒泡
清晰印证:捕获自上而下,冒泡自下而上,目标阶段夹在中间。

















