前端一面:DOM 事件流、事件委托与常见坑

DOM 事件相关的一面题,大多围绕“事件流三阶段 + 事件委托 + 几个常见坑”展开。这一篇尝试用一两个图景把事件流讲清楚,再配几道典型题的参考答案。

在标准 DOM 事件模型中,一次事件传播可以分为三个阶段:

  1. 捕获阶段(capturing):事件从 window 一路向下,经过每个祖先节点,到达目标元素;
  2. 目标阶段(target):在目标元素上触发事件处理;
  3. 冒泡阶段(bubbling):事件从目标元素向上冒泡,依次经过各个祖先节点,直到 window

在调用 addEventListener 时,可以通过第三个参数控制监听的是捕获还是冒泡阶段:

1element.addEventListener("click", handler, true);  // 捕获阶段
2element.addEventListener("click", handler, false); // 冒泡阶段(默认)

一面里常见的考察点是:

  • 能否用自己的话描述这三个阶段;
  • 知不知道如何在捕获/冒泡阶段监听事件,以及 event.stopPropagation() 的作用。

事件委托 的核心思路是:
不在每个子元素上单独绑定事件,而是在它们的共同父元素上绑定一次,通过事件冒泡来统一处理。

例如有一个长列表,每个项都需要响应点击:

1const list = document.querySelector(".list");
2
3list.addEventListener("click", (event) => {
4  const item = event.target.closest(".item");
5  if (!item || !list.contains(item)) return;
6
7  console.log("clicked item:", item.dataset.id);
8});

好处包括:

  • 减少事件监听器数量,尤其是动态增删子元素时,无需重复绑定/解绑;
  • 便于统一管理逻辑(例如打埋点、权限控制)。

在一面回答事件委托时,讲清“利用冒泡,把监听器挂在共同父级上,再通过 event.target/closest 找出真正触发元素”即可。

在事件相关题目里,面试官有时会结合几个小坑来考察基础功:

  • event.stopPropagation() vs event.stopImmediatePropagation()

    • stopPropagation 阻止事件继续向上冒泡,但不会影响当前元素上其它监听器;
    • stopImmediatePropagation 不仅阻止冒泡,还会阻止当前元素上后续的监听器执行。
  • 阻止默认行为

    • event.preventDefault() 用于阻止默认行为(例如链接跳转、表单提交);
    • 不会阻止事件冒泡,需要的话还要额外调用 stopPropagation
  • event.target vs event.currentTarget

    • target 是实际触发事件的元素;
    • currentTarget 是当前正在执行监听器的元素(通常是你绑定监听器的那个)。

这些点往往被用在“找输出顺序”或“分析点击行为”的小题里。

参考答案要点:

  • DOM 事件流分为捕获阶段、目标阶段和冒泡阶段:
    • 捕获阶段事件从上到下传播到目标;
    • 在目标元素上触发事件处理;
    • 然后在冒泡阶段从下往上返回到根节点;
  • 在浏览器中,默认使用冒泡阶段监听事件:addEventListener("click", handler)
  • 如果需要在捕获阶段监听,可以传第三个参数 true{ capture: true }

如果面试官追问“什么场景会用捕获?”可以举:

  • 例如在全局层面对某些事件做“最优先”的处理(如埋点、统一拦截),或者在某些冒泡逻辑之前先做一次筛选。

参考答案要点:

  • 原理:利用事件冒泡,只在父元素上绑定一次监听器,在回调中通过 event.target/closest 判断具体是哪个子元素被点击;
  • 优点:
    • 减少监听器数量,降低内存开销;
    • 动态增删子元素时无需反复绑定/解绑事件;
    • 方便统一处理逻辑(如打点、权限判断)。

可以简要提一下潜在的注意点,例如:

  • 需要确保 event.target 真的是你关心的那类子元素(可以通过 closest + contains 做保护)。
1<div id="outer">
2  <button id="inner">Click me</button>
3</div>
1outer.addEventListener("click", () => {
2  console.log("outer");
3});
4
5inner.addEventListener("click", () => {
6  console.log("inner");
7});

参考答案:

  • 点击按钮时,会先输出 inner,再输出 outer
    • 因为 inner 上的监听器在目标阶段触发,之后事件冒泡到 outer 上触发其监听器;
  • 如果希望阻止冒泡,可以在 inner 的回调里调用:
1inner.addEventListener("click", (event) => {
2  console.log("inner");
3  event.stopPropagation(); // 阻止事件冒泡到 outer
4});

可以顺带提一下:preventDefault 只是阻止默认行为,不会阻止事件继续冒泡。

在一面里遇到 DOM 事件相关的问题时,除了写出正确代码/输出顺序,更加分的是:

  • 你能用自己的话把事件流“三阶段”讲清楚;
  • 能自然解释事件委托背后的冒泡机制;
  • 面对常见坑(阻止冒泡/默认行为、target vs currentTarget),有一套稳定的判断/排查思路。

这类题的难度往往不在于 API 多复杂,而在于你是否真的熟悉浏览器事件系统,而不仅仅是“碰巧写过”。