Skip to main content
我对事件流的理解分几次补上的:先记住冒泡和捕获,后来写事件委托,最后才把浏览器派发过程串起来。

Q1: DOM 事件的历史?为什么要有事件模型?

1

1995年: 事件处理器的诞生

Netscape 2.0 引入了事件处理机制,采用捕获模型(事件从 window 向目标元素传播)。微软 IE 3.0 则采用冒泡模型(事件从目标元素向 window 传播)。两种模型完全相反,开发者苦不堪言。
2

1997年: DOM Level 1 规范

W3C 发布 DOM Level 1,但没有统一事件模型。不同浏览器的事件处理方式完全不同,开发者需要写大量兼容代码。
3

2000年: DOM Level 2 统一事件流

W3C 发布 DOM Level 2 Events 规范,统一了事件流:捕获阶段 → 目标阶段 → 冒泡阶段。同时引入了 addEventListener API,取代了 onclick 等内联事件处理器。
DOM 事件模型解决的核心问题:
DOM 事件流 = 捕获阶段 + 目标阶段 + 冒泡阶段,开发者可以在任意阶段处理事件。

🚫 没有统一的事件模型

一家公司有两个部门(捕获部门和冒泡部门),每个部门都有自己的审批流程。你提交一份申请,两个部门的流程完全不同——你需要分别按照两个部门的规则提交两次申请,才能完成审批。

✅ 引入统一的事件流

公司统一了审批流程:先经过捕获部门(审核资格),再经过目标部门(处理业务),最后经过冒泡部门(归档记录)。你只需要提交一次申请,流程自动走完。你还可以选择在任意阶段插入自己的审批节点(事件监听器)。
回答时先说”为什么需要事件模型”(Netscape vs IE 的兼容性问题),再说”事件流的三个阶段”。想听的是你对事件传播完整过程的理解,而不是只记住”冒泡”。

Q2: 事件流的三个阶段

事件流是事件在 DOM 树中的传播路径,分为三个阶段:
在目标阶段(事件到达目标元素时),事件监听器的执行顺序不区分捕获和冒泡,而是按照注册顺序执行。
事件流就像快递配送——快递从仓库(window)出发,经过各个中转站(捕获阶段),到达收件人(目标阶段)。收件人拆开包裹后,快递单信息逐级回传到仓库(冒泡阶段)。你可以在快递到达你家之前拦截(捕获阶段),也可以在快递离开你家之后处理(冒泡阶段)。

事件监听器的注册方式


Q3: 事件委托(Event Delegation)

事件委托是利用事件冒泡机制,把子元素的事件处理委托给父元素。
e.target 是实际触发事件的元素,e.currentTarget 是绑定事件监听器的元素。在事件委托中,必须用 e.target 来判断是哪个子元素触发了事件。
事件委托就像公司前台——你不需要给每个员工都配一个秘书(事件监听器)。所有访客都先到前台(父元素),前台根据访客要找的人(e.target)来分发。这样即使公司新招了员工(动态添加元素),也不需要额外配秘书。

事件委托的适用场景


Q4: 事件对象(Event Object)的核心属性

事件对象包含了事件的所有信息,是事件处理的核心。
这两个方法经常被混淆,但它们的作用完全不同:

事件对象的常用方法


Q5: 自定义事件(Custom Events)

除了浏览器原生事件,开发者还可以创建和派发自定义事件。
自定义事件常用于组件间通信,尤其是在不使用框架的情况下:

Q6: 事件循环中的事件处理

事件处理和 Event Loop 密切相关——浏览器在 Event Loop 的特定阶段处理事件。
事件处理器中的同步代码会阻塞渲染——如果事件处理器执行时间过长,页面会卡顿。

Q7: 事件性能优化

事件处理不当会导致性能问题,以下是常见的优化手段。

1. 防抖(Debounce)

2. 节流(Throttle)

对于 touchstart、touchmove、scroll 等高频事件,浏览器会等待事件处理器执行完才能确定是否需要滚动。如果事件处理器耗时过长,页面会卡顿。

3. 事件委托 vs 直接绑定


Q8: 事件捕获的实际应用

事件捕获在实际开发中用得较少,但在某些场景下非常有用。
捕获阶段常用于:
  1. 全局事件拦截:在事件到达目标元素之前进行统一处理
  2. 权限控制:根据用户权限决定是否允许事件继续传播
  3. 错误捕获:在捕获阶段捕获错误,比冒泡阶段更早

底层源码:浏览器是如何实现事件系统的?

浏览器的事件系统在 Blink 中实现,核心是事件派发器(EventDispatcher)(详见 Blink 源码:third_party/blink/renderer/core/dom/events/):
这段代码做了 3 件事:
  1. 在捕获阶段,从 window 向目标元素传播,触发捕获阶段的事件监听器
  2. 在目标阶段,触发目标元素上的事件监听器(不区分捕获/冒泡)
  3. 在冒泡阶段,从目标元素向 window 传播,触发冒泡阶段的事件监听器
Blink 用哈希表存储事件监听器,按事件类型分组:
事件监听器按注册顺序存储,执行时根据当前事件阶段(捕获/冒泡)过滤。

十、及格回答 vs 满分回答

问题:“说说 DOM 事件流,事件委托是怎么回事?”

及格回答(60-70分):
“事件流分为捕获和冒泡两个阶段。事件委托就是把子元素的事件绑定到父元素上,利用冒泡机制处理。这样可以减少事件监听器的数量,提高性能。”
问题出在:知道规则但不理解细节。 满分回答(90-100分):
DOM 事件流分为三个阶段:捕获阶段(从 window 到目标元素的父级)、目标阶段(目标元素本身)、冒泡阶段(从目标元素的父级到 window)。在捕获和冒泡阶段,事件监听器按照注册顺序执行;在目标阶段,不区分捕获和冒泡,按注册顺序执行。[1]
事件委托的本质是利用冒泡机制,把子元素的事件处理委托给父元素。这样做的好处是:1)减少事件监听器数量,降低内存开销;2)动态添加的子元素自动拥有事件处理能力;3)统一管理事件,便于维护。但要注意 e.target 和 e.currentTarget 的区别——e.target 是实际触发事件的元素,e.currentTarget 是绑定事件监听器的元素。[2]
实际项目中,事件委托常用于列表项点击、表格行操作等场景。对于高频事件(如 scroll、touchmove),需要配合防抖/节流优化。另外,passive: true 选项可以告诉浏览器不会调用 preventDefault,让浏览器立即开始滚动,提升页面流畅度。[3]
在 React 中,事件委托是默认行为——React 17 之前把所有事件委托到 document,React 17 之后委托到根节点(#root)。这就是为什么 React 的合成事件(SyntheticEvent)和原生 DOM 事件的执行顺序有时会让人困惑。

常见追问

答题要点: 捕获从外到内,冒泡从内到外;目标阶段不区分。高分回答: “事件流分为三个阶段:
  1. 捕获阶段:事件从 window 向目标元素传播,先触发父元素的捕获监听器,再触发子元素的
  2. 目标阶段:事件到达目标元素,不区分捕获/冒泡,按注册顺序执行
  3. 冒泡阶段:事件从目标元素向 window 传播,先触发父元素的冒泡监听器,再触发祖父元素的
addEventListener 的第三个参数:
  • true:在捕获阶段触发
  • false(默认):在冒泡阶段触发”
答题要点: React 17 之前委托到 document,17 之后委托到根节点。高分回答: “React 的合成事件(SyntheticEvent)是对原生 DOM 事件的封装。执行顺序取决于 React 版本:React 16(委托到 document):
React 17+(委托到根节点 #root):
React 17 之后,合成事件和原生事件的执行顺序更符合直觉。”
答题要点: 防抖是”等待”,节流是”冷却”。高分回答: “防抖(Debounce)和节流(Throttle)都是限制函数执行频率的方法:防抖:事件触发后等待 N 毫秒,如果期间再次触发则重新计时。
节流:N 毫秒内只执行一次。
区别:
  • 防抖:用户停止操作后才执行(延迟执行)
  • 节流:用户操作期间定期执行(固定频率)”
答题要点: 告诉浏览器不会调用 preventDefault,可以立即滚动。高分回答: “passive: true 是 addEventListener 的一个选项,用于告诉浏览器:这个事件监听器不会调用 e.preventDefault()。为什么需要这个选项? 对于 touchstart、touchmove、scroll 等高频事件,浏览器需要等待事件处理器执行完才能确定是否需要滚动。如果事件处理器耗时过长,页面会卡顿。
注意:Chrome 已经默认把 document 级别的 touchstart、touchmove 设为 passive: true。”
答题要点: 优点是减少监听器数量、支持动态元素;缺点是不支持所有事件。高分回答: “事件委托的优点:
  1. 减少事件监听器数量:100 个子元素只需要 1 个事件监听器
  2. 支持动态元素:新添加的子元素自动拥有事件处理能力
  3. 统一管理:所有事件在一个地方处理,便于维护
事件委托的缺点:
  1. 不支持所有事件:focus、blur、mouseenter、mouseleave 等事件不冒泡
  2. 性能问题:如果父元素层级很深,事件需要传播很多层
  3. 代码复杂度:需要判断 e.target,代码可能变得复杂
答题要点: stopPropagation 和 stopImmediatePropagation 的区别。高分回答: “阻止事件传播有两种方法:
区别:
  • stopPropagation:阻止事件传播到其他元素,但同元素上的其他监听器仍然执行
  • stopImmediatePropagation:阻止事件传播到其他元素,同时阻止同元素上的其他监听器
还有 preventDefault:阻止浏览器默认行为(如链接跳转),但不影响事件传播。”
答题要点: 不移除事件监听器会导致内存泄漏,尤其是单页应用。高分回答: “事件监听器如果不手动移除,会导致内存泄漏:
React 中的正确做法:
Vue 中的正确做法:
答题要点: 用发布-订阅模式实现,支持 on、off、emit。高分回答: “事件总线的核心是发布-订阅模式:
Vue 2 中的 $on、$off、$emit 就是事件总线的实现。Vue 3 移除了这个 API,推荐使用外部库(如 mitt)。“

延伸阅读与引用参考