事件传播 vs 事件委托
Event propagation vs Event delegation
一句话:传播是浏览器的机制(捕获 → 目标 → 冒泡,见 #380);委托是我们利用这个机制的技巧—— 把监听器挂在父元素上,通过 e.target 判断实际点了哪个子元素。
委托解决两个问题:
- 监听器数量—— 1000 行的表格挂 1000 个监听器,内存和绑定开销都很可观;委托只要 1 个。
- 动态元素—— 后来才插进来的子元素自动就有了行为,不用重新绑定。这一条往往更实用。
写法的关键是 e.target.closest()—— 因为用户可能点在按钮里的<span> 上,直接比 e.target.matches() 会漏。
会追问(重点):「React 的事件是委托的吗?」——是,而且这是它的核心设计:React 把事件统一挂在根容器上(React 17 之前挂在 document,17 之后挂到 root 节点,这是为了支持一个页面里多个 React 版本共存),然后用合成事件(SyntheticEvent)模拟一套跨浏览器一致的事件系统。
推论:React 17 起,e.stopPropagation()拦得住 React 组件之间的传播,也拦得住挂在 root 之外(document、window)的原生监听器;拦不住的是挂在目标和 root 之间的原生监听器—— 它们在事件冒到 root 之前就执行完了。这个点答出来会很加分。
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: since React 17, e.stopPropagation() stops propagation between React components and also stops native listeners above the root (document, window); what it cannot stop are native listeners between the target and the root, because they run before the event bubbles up to the root. Landing this point scores well.