DrillLab
第 2 / 105 道2 / 105 · #380

事件冒泡 vs 事件捕获

Event bubbling vs Event capturing

先自己答,再往下看Answer it yourself first

一句话:一次点击在 DOM 里走三段 —— 先从 window 往下到目标(捕获), 在目标上触发(目标阶段),再从目标往上回到window(冒泡)。捕获从外到内,冒泡从内到外。

addEventListener 第三个参数决定你在哪一段听: 默认 false 听冒泡,true(或 { capture: true })听捕获。

为什么默认是冒泡?因为绝大多数时候你想知道的是 「用户点了什么」,从内往外传最自然 —— 而且这才让事件委托成为可能(见 #289)。

会追问:「怎么阻止?」

  • e.stopPropagation()—— 别再往上(或往下)传了。
  • e.stopImmediatePropagation()—— 连同一个元素上的其他监听器都别跑了。
  • e.preventDefault()—— 阻止的是默认行为(链接跳转、表单提交), 和传播完全无关。这两个最容易混,别答错。

还会追问:「有哪些事件不冒泡?」——focusblurloadmouseentermouseleave。 需要委托时用它们的冒泡版:focusin /focusout / mouseover /mouseout

In one line: a click travels the DOM in three phases — down from window to the target (capture), fires on the target, then back up to window (bubble). Capture goes outside-in, bubbling goes inside-out.

The third argument to addEventListener picks the phase you listen in: false (the default) is bubbling, true (or { capture: true }) is capture.

Why is bubbling the default? Because what you almost always want to know is “what did the user click”, and inside-out is the natural direction for that — it is also what makes event delegation possible (see #289).

Follow-up: “How do you stop it?”

  • e.stopPropagation() — stop travelling further up (or down).
  • e.stopImmediatePropagation() — also skip other listeners on the same element.
  • e.preventDefault() — cancels the default behaviour (link navigation, form submit) and has nothing to do with propagation. These two get mixed up most often; do not confuse them.

Also asked: “Which events do not bubble?” —focus, blur, load, mouseenter, mouseleave. When you need to delegate, use their bubbling counterparts: focusin / focusout / mouseover / mouseout.

JavaScript三个阶段的完整顺序The full order of the three phases示意Illustrative
1// <div id="outer"><div id="inner"><button id="btn">点我</button></div></div>
2
3outer.addEventListener("click", () => console.log("outer 捕获"), true);
4inner.addEventListener("click", () => console.log("inner 捕获"), true);
5btn .addEventListener("click", () => console.log("btn 目标"));
6inner.addEventListener("click", () => console.log("inner 冒泡"));
7outer.addEventListener("click", () => console.log("outer 冒泡"));
8
9// 点击 btn 的输出顺序:
10// outer 捕获 -> inner 捕获 -> btn 目标 -> inner 冒泡 -> outer 冒泡
1// <div id="outer"><div id="inner"><button id="btn">Click me</button></div></div>
2
3outer.addEventListener("click", () => console.log("outer capture"), true);
4inner.addEventListener("click", () => console.log("inner capture"), true);
5btn .addEventListener("click", () => console.log("btn target"));
6inner.addEventListener("click", () => console.log("inner bubble"));
7outer.addEventListener("click", () => console.log("outer bubble"));
8
9// Output order when btn is clicked:
10// outer capture -> inner capture -> btn target -> inner bubble -> outer bubble
面试里画得出这个顺序,基本就过了。注意目标元素上的监听器不分捕获/冒泡,按注册顺序执行。If you can draw this order in an interview, that question is handled. One detail: on the target element itself there is no capture or bubble distinction, so those listeners run in the order they were registered.