第 19 章:合成事件系统——React 19 的委托机制
你在 JSX 里写 onClick,React 并没有给每个元素 addEventListener。第 18 章说过,事件走全局委托。本章看完整机制:什么时候绑、绑在哪、触发时怎么找到你的 handler。
先记住这一句:React 把几乎所有事件统一委托到根容器(
createRoot时绑一次),触发时从触发点反查 fiber,从下往上收集所有 handler 依次执行。这就是"合成事件"的核心。
一图流:一次点击的完整路径
什么时候绑:createRoot 时绑一次
第 1 章说过,createRoot 会调用 listenToAllSupportedEvents(DOMPluginEventSystem.js:432)。它把所有支持的原生事件在根容器上各绑一遍(bubble + capture 两遍):
allNativeEvents.forEach(domEventName => {
if (!nonDelegatedEvents.has(domEventName)) {
listenToNativeEvent(domEventName, false, rootContainerElement); // bubble
}
listenToNativeEvent(domEventName, true, rootContainerElement); // capture
});
有两点值得注意:
- 只绑一次。整棵应用只有一个根,所有事件委托在它上面,而不是每个元素各绑各的。元素多了,绑定的监听器数量不涨。
nonDelegatedEvents例外。有些事件不冒泡(如scroll、load、mouseenter),没法靠根容器委托,这些就直接绑到目标元素上。这是"几乎所有事件"而不是"所有"的原因。
触发时:DOM 反查 fiber
事件触发后,React 怎么知道"这个点击属于哪个组件"?靠 DOM 和 fiber 的双向映射(第 1 章 markContainerAsRoot 埋的底子):从事件目标 DOM 节点,反查它对应的 fiber,然后沿 return 链(第 3 章)往上走,把沿途 fiber 里注册了同名 handler 的都收进数组。收集逻辑在 dispatchEventsForPlugins(325),入口是 584。
收集完,按从内到外的顺序依次调用。这就是 React 里 stopPropagation 能拦住"外层组件"的原因:外层的 handler 在数组后面,还没轮到就被标记停掉了。
为什么要这套东西
- 性能:监听器数量与应用规模无关,只跟事件类型有关。
- 统一行为:
e.preventDefault、e.stopPropagation跨浏览器行为一致;事件池、合成对象由 React 统一管理。 - 架构干净:事件系统不依赖"每个元素上的回调",为并发渲染的"延迟处理事件"(第 23 章)留了空间。
动手实验
- 数监听器:渲染 1000 个
onClick元素,在根容器上打断点确认只绑了一次,而不是 1000 次。 - 看收集顺序:在
dispatchEventsForPlugins(325)打断点,嵌套三层组件各写onClick,点最内层,观察收集到的 handler 数组是从内到外的。 - 验证 nonDelegated:给元素绑
onScroll,看它是绑在元素上而不是根上(对照源码里nonDelegatedEvents的处理)。
小结
- 委托:
createRoot时listenToAllSupportedEvents(432)把事件统一绑到根容器,bubble + capture 各一遍。 - 例外:不冒泡的事件(scroll、load 等)直接绑到元素。
- 触发:DOM 反查 fiber,沿
return链从下往上收集 handler 依次执行。 - 意义:性能(监听器数量与规模无关)、跨浏览器统一、为并发留空间。
- 下一步:事件触发的 setState 会走更新,effect 又是怎么在 commit 后执行的,第 20 章。