Skip to main content

第 19 章:合成事件系统——React 19 的委托机制

你在 JSX 里写 onClick,React 并没有给每个元素 addEventListener。第 18 章说过,事件走全局委托。本章看完整机制:什么时候绑、绑在哪、触发时怎么找到你的 handler

先记住这一句:React 把几乎所有事件统一委托到根容器createRoot 时绑一次),触发时从触发点反查 fiber,从下往上收集所有 handler 依次执行。这就是"合成事件"的核心。

一图流:一次点击的完整路径

什么时候绑:createRoot 时绑一次

第 1 章说过,createRoot 会调用 listenToAllSupportedEventsDOMPluginEventSystem.js:432)。它把所有支持的原生事件在根容器上各绑一遍(bubble + capture 两遍):

allNativeEvents.forEach(domEventName => {
if (!nonDelegatedEvents.has(domEventName)) {
listenToNativeEvent(domEventName, false, rootContainerElement); // bubble
}
listenToNativeEvent(domEventName, true, rootContainerElement); // capture
});

有两点值得注意:

  1. 只绑一次。整棵应用只有一个根,所有事件委托在它上面,而不是每个元素各绑各的。元素多了,绑定的监听器数量不涨。
  2. nonDelegatedEvents 例外。有些事件不冒泡(如 scrollloadmouseenter),没法靠根容器委托,这些就直接绑到目标元素上。这是"几乎所有事件"而不是"所有"的原因。

触发时:DOM 反查 fiber

事件触发后,React 怎么知道"这个点击属于哪个组件"?靠 DOM 和 fiber 的双向映射(第 1 章 markContainerAsRoot 埋的底子):从事件目标 DOM 节点,反查它对应的 fiber,然后沿 return 链(第 3 章)往上走,把沿途 fiber 里注册了同名 handler 的都收进数组。收集逻辑在 dispatchEventsForPlugins325),入口是 584

收集完,按从内到外的顺序依次调用。这就是 React 里 stopPropagation 能拦住"外层组件"的原因:外层的 handler 在数组后面,还没轮到就被标记停掉了。

为什么要这套东西

  • 性能:监听器数量与应用规模无关,只跟事件类型有关。
  • 统一行为e.preventDefaulte.stopPropagation 跨浏览器行为一致;事件池、合成对象由 React 统一管理。
  • 架构干净:事件系统不依赖"每个元素上的回调",为并发渲染的"延迟处理事件"(第 23 章)留了空间。

动手实验

  1. 数监听器:渲染 1000 个 onClick 元素,在根容器上打断点确认只绑了一次,而不是 1000 次。
  2. 看收集顺序:在 dispatchEventsForPlugins(325)打断点,嵌套三层组件各写 onClick,点最内层,观察收集到的 handler 数组是从内到外的。
  3. 验证 nonDelegated:给元素绑 onScroll,看它是绑在元素上而不是根上(对照源码里 nonDelegatedEvents 的处理)。

小结

  • 委托createRootlistenToAllSupportedEvents(432)把事件统一绑到根容器,bubble + capture 各一遍。
  • 例外:不冒泡的事件(scroll、load 等)直接绑到元素。
  • 触发:DOM 反查 fiber,沿 return 链从下往上收集 handler 依次执行。
  • 意义:性能(监听器数量与规模无关)、跨浏览器统一、为并发留空间。
  • 下一步:事件触发的 setState 会走更新,effect 又是怎么在 commit 后执行的,第 20 章。