DrillLab
第 46 / 105 道46 / 105 · #289

事件传播 vs 事件委托

Event propagation vs Event delegation

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

一句话:传播是浏览器的机制(捕获 → 目标 → 冒泡,见 #380);委托是我们利用这个机制的技巧—— 把监听器挂在父元素上, 通过 e.target 判断实际点了哪个子元素。

委托解决两个问题:

  • 监听器数量—— 1000 行的表格挂 1000 个监听器, 内存和绑定开销都很可观;委托只要 1 个。
  • 动态元素—— 后来才插进来的子元素自动就有了行为, 不用重新绑定。这一条往往更实用。

写法的关键是 e.target.closest()—— 因为用户可能点在按钮里的<span> 上, 直接比 e.target.matches() 会漏。

会追问(重点):「React 的事件是委托的吗?」——,而且这是它的核心设计: React 把事件统一挂在根容器上 (React 17 之前挂在 document, 17 之后挂到 root 节点,这是为了支持一个页面里多个 React 版本共存), 然后用合成事件(SyntheticEvent)模拟一套跨浏览器一致的事件系统。
推论:所以在 React 里e.stopPropagation()拦得住 React 组件之间的传播, 但拦不住原生监听器—— 因为原生的已经先跑完了。 这个点答出来会很加分。

In one line: propagation is the browser’s mechanism (capture → target → bubble, see #380); delegation is the trick we play with it — put the listener on the parent and use e.target to work out which child was really clicked.

Delegation solves two problems:

  • The number of listeners — a 1000-row table with 1000 listeners costs real memory and real binding time; delegation needs one.
  • Dynamic elements — children inserted later already have the behaviour, with nothing to rebind. In practice this is often the bigger win.

The key to writing it is e.target.closest() — the user may have clicked a <span> inside the button, so a bare e.target.matches() misses it.

Follow-up, and this is the one that matters: “Are React events delegated?” — yes, and it is central to the design: React attaches events to the root container (to document before React 17, to the root node from 17 onwards, so that several React versions can coexist on one page), then wraps them in a SyntheticEvent to present one event system that behaves the same across browsers.
The consequence: inside React, e.stopPropagation() does stop propagation between React components, but it cannot stop a native listener — the native one already ran. Landing this point scores well.

JavaScript事件委托的标准写法The standard way to write event delegation示意Illustrative
1// ✗ 每一行一个监听器,而且新增的行没有行为
2document.querySelectorAll("tr .del").forEach((btn) =>
3 btn.addEventListener("click", onDelete),
4);
5
6// ✓ 委托:一个监听器,新增的行自动有行为
7table.addEventListener("click", (e) => {
8 // closest 而不是 matches —— 用户可能点在按钮里的图标上
9 const btn = e.target.closest(".del");
10 if (!btn) return; // 点到空白处,直接退出
11 onDelete(btn.dataset.id);
12});
13
14// target vs currentTarget
15// e.target = 真正被点的元素(可能是按钮里的 span)
16// e.currentTarget = 监听器挂在哪(这里永远是 table)
1// ✗ One listener per row, and rows added later have no behaviour
2document.querySelectorAll("tr .del").forEach((btn) =>
3 btn.addEventListener("click", onDelete),
4);
5
6// ✓ Delegation: one listener, and new rows behave correctly on their own
7table.addEventListener("click", (e) => {
8 // closest, not matches —— the user may click the icon inside the button
9 const btn = e.target.closest(".del");
10 if (!btn) return; // a click on empty space just returns
11 onDelete(btn.dataset.id);
12});
13
14// target vs currentTarget
15// e.target = the element actually clicked (maybe a span inside the button)
16// e.currentTarget = where the listener is attached (always the table here)