React 事件系统设计:如果要为 React 增加一个自定义的合成事件,你需要修改源码中的哪些插件注册表?

各位前端界的同仁,还有那些立志要摸透 React 内部黑箱的勇士们,大家好。

今天我们不聊业务,不聊架构模式,也不聊如何用 React 写出“高大上”的组件库。今天,我们要钻进 React 的腹地,去解剖它的心脏——事件系统

你可能会问:“不就是 onClick={handleClick} 吗?有什么好解剖的?” 哼,肤浅。你以为你只是在写代码,其实你是在和浏览器原生的 DOM API 打交道,而 React 在中间充当了一个极其复杂的“翻译官”和“中间人”。这个中间人叫 SyntheticEvent,也就是我们常说的“合成事件”。

如果你想让 React 支持一个它原本不认识的事件,比如 onConfettiExplosion(恭喜你,放彩带爆炸),你以为是加一行配置就完事了?不,那是做梦。你需要改源码,而且是动刀子地改。

那么,问题来了:如果要为 React 增加一个自定义的合成事件,你需要修改源码中的哪些插件注册表?这就像是在乐高城堡里加一座新的塔楼,你得知道地基在哪里,砖块在哪里,图纸在哪里。

来,把咖啡喝好,我们开始这场源码深潜。

一、 React 事件系统的“三巨头”

在动手之前,你得先理解 React 事件系统的架构。它不是凭空出来的,它是由三个核心模块组成的“三巨头”:

  1. EventPluginRegistry(事件插件注册表): 这是门卫。它负责登记所有的插件,告诉系统谁是谁。如果没有它,React 就不知道 onClick 是谁负责的,onChange 又是谁负责的。它是整个系统的“指挥中心”。
  2. EventPluginHub(事件插件枢纽): 这是大脑。它负责处理事件的核心逻辑,比如事件池的分配、事件的冒泡和捕获、以及把原生事件转换成合成事件。
  3. EventPluginModule(具体的插件): 这是干活的工人。比如 SimpleEventPlugin 负责点击,ChangeEventPlugin 负责输入框变化。每个插件都有自己的职责。

我们要做的自定义事件,本质上就是创造一个新的“工人”,然后把它塞进“门卫”的名单里。

二、 核心痛点:SimpleEventPlugin

既然我们要加自定义事件,那么绝大多数自定义事件(比如 onMyCustomEvent)都是直接映射到浏览器原生的某个事件上的。比如我们想监听 DOM 的 scroll 事件,我们就希望 React 能把 onScroll 映射过去。

这时候,SimpleEventPlugin 就是那个最需要被修改的插件。

为什么?因为它是 React 事件系统的“万恶之源”,也是“万善之源”。它定义了 React 如何将 DOM 的原生事件映射成 React 的合成事件。

在 React 源码的 packages/react-dom/src/events/SimpleEventPlugin.js 中,你会看到这样一个核心对象:

const topLevelEventsToReactNames = {
  topAbort: 'onAbort',
  topAbort: 'onAbort',
  topAnimationEnd: 'onAnimationEnd',
  topAnimationIteration: 'onAnimationIteration',
  topAnimationStart: 'onAnimationStart',
  topBlur: 'onBlur',
  topCanPlay: 'onCanPlay',
  topCanPlayThrough: 'onCanPlayThrough',
  topChange: 'onChange',
  topClick: 'onClick',
  // ... 还有几百行类似的映射
  topScroll: 'onScroll',
  // ... 
};

这就是注册表!这就是你要修改的地方。

如果你想加一个 onConfettiExplosion,你必须在 topLevelEventsToReactNames 里加一行:

const topLevelEventsToReactNames = {
  // ... 现有的
  topConfettiExplosion: 'onConfettiExplosion', // 我们的新朋友
};

但是,光加这一行还不够。这只是告诉系统“当 DOM 发生 topConfettiExplosion 时,React 的名字叫 onConfettiExplosion”。这就像你给新来的员工发了工牌,但他还没领工具呢。

三、 修改 EventPluginRegistry:插队与排序

光改 SimpleEventPlugin 还不够,你还得去 EventPluginRegistry 里把你的新插件“注册”进去。

源码位置:packages/react-dom/src/events/EventPluginRegistry.js

这个文件里有一个至关重要的变量 EventPluginOrder。它定义了事件处理的顺序。React 必须按照这个顺序来调用插件,因为有些插件必须先于其他插件处理事件(比如 EnterLeaveEventPlugin 必须在 SimpleEventPlugin 之前处理,否则冒泡逻辑就乱了)。

// 这里的顺序是硬编码的,如果你加的插件依赖某些逻辑,必须调整这里
const EventPluginOrder = [
  'ResponderEventPlugin',
  'SimpleEventPlugin',
  'EnterLeaveEventPlugin',
  'ChangeEventPlugin',
  'SelectEventPlugin',
  'BeforeInputEventPlugin',
];

你需要在这里加上你新插件的名字。假设你的插件叫 MyCustomEventPlugin

const EventPluginOrder = [
  'ResponderEventPlugin',
  'SimpleEventPlugin',
  'EnterLeaveEventPlugin',
  'ChangeEventPlugin',
  'SelectEventPlugin',
  'BeforeInputEventPlugin',
  'MyCustomEventPlugin', // 欢迎加入,请站好队
];

四、 实战演练:手写一个“彩带爆炸”插件

理论讲得太多容易犯困,我们直接上代码。假设我们要监听一个自定义的 onConfettiExplosion 事件。

第一步:创建插件文件 MyCustomEventPlugin.js

这个插件需要实现 React 插件的标准接口:eventTypes(定义事件名)和 extractEvents(从 DOM 事件中提取合成事件)。

// packages/react-dom/src/events/MyCustomEventPlugin.js

import { SyntheticEvent } from 'react';
import { EventConstants } from 'fbjs/lib/EventConstants';
import { EventPropagators } from 'react-dom/lib/EventPropagators';
import { getNativeEventTarget } from 'react-dom/lib/getEventTarget';

// 1. 定义事件类型
const MyCustomEventInterface = {
  bubbles: true,
  cancelable: true,
  composed: false,
};

// 2. 生成事件类型常量(React 内部用这个来校验)
const topLevelTypes = EventConstants.topLevelTypes;

// 3. 定义插件
const MyCustomEventPlugin = {
  eventTypes: {
    onConfettiExplosion: {
      phasedRegistrationNames: {
        bubbled: 'onConfettiExplosion',
        captured: 'onConfettiExplosionCapture',
      },
      // 这里可以定义依赖关系,比如这个事件依赖于哪些原生事件
      // dependencies: [topLevelTypes.topScroll], 
    },
  },

  // 4. 插件顺序(必须与 EventPluginRegistry 中的顺序一致)
  extractEvents: function(
    topLevelType: mixed,
    targetInst: null | object,
    nativeEvent: mixed,
    nativeEventTarget: null | DOMEventTarget,
  ): null | ReactEvent {
    // 只有当原生事件是我们定义的 topConfettiExplosion 时才处理
    if (topLevelType === topLevelTypes.topConfettiExplosion) {
      // 创建合成事件
      // 注意:这里我们直接复用浏览器原生的 event 对象,或者创建一个新对象
      // React 会通过 EventPluginHub 来管理这个对象的生命周期

      const event = new SyntheticEvent(
        MyCustomEventInterface,
        nativeEvent,
        nativeEventTarget,
      );

      // 这里可以添加一些自定义逻辑,比如判断是否真的需要爆炸
      // if (shouldExplode(nativeEvent)) { ... }

      return event;
    }
    return null;
  },
};

export default MyCustomEventPlugin;

第二步:修改 EventPluginRegistry 注册表

正如前面所说,你必须把这个插件扔进 plugins 数组里。

packages/react-dom/src/events/EventPluginRegistry.js 中:

import MyCustomEventPlugin from './MyCustomEventPlugin';

// ... 在文件底部
const plugins = [
  // ... 其他插件
  MyCustomEventPlugin, // 挂上去
];

// ... 在 installPlugins 函数里
export function installPlugins() {
  // ... 现有的逻辑
  plugins.forEach(plugin => {
    // 这里会遍历 plugins,调用每个插件的 install 方法
    // 如果你的插件有 install 方法,这里就是入口
    if (plugin.install) {
      plugin.install();
    }
  });
}

第三步:修改 SimpleEventPlugin(关键中的关键)

现在,你的插件已经注册了,它也定义了 eventTypes。但是,React 在渲染的时候,是如何把 JSX 里的 onConfettiExplosion 绑定到 DOM 上的呢?

这就要回到 SimpleEventPlugin。虽然我们主要修改的是注册表,但 SimpleEventPlugin 负责了最繁琐的“映射工作”。

packages/react-dom/src/events/SimpleEventPlugin.js 的底部,有一个 registrationNameModules 对象。它记录了每个 React 事件名对应的是哪个插件。

const registrationNameModules = {
  onClick: SimpleEventPlugin,
  onChange: ChangeEventPlugin,
  // ... 现有的
  onConfettiExplosion: MyCustomEventPlugin, // 必须加上这一行!
};

这行代码的意思是:“嘿,如果有人用了 onConfettiExplosion,别去查浏览器原生事件了,去找 MyCustomEventPlugin 去提取事件。”

五、 深入 DOM 层:ReactBrowserEventEmitter

光有插件还不够。插件只负责“提取”和“处理”事件。当用户点击页面时,浏览器会触发原生的 click 事件,React 需要拦截这个事件,然后把它分发给对应的插件。

这个拦截工作发生在 ReactBrowserEventEmitter 中。

源码位置:packages/react-dom/src/events/ReactBrowserEventEmitter.js

这里定义了 React 如何在顶层 DOM 节点上绑定监听器。默认情况下,React 会把所有事件绑定在 document 上,这就是著名的事件委托

如果你想监听一个自定义事件,你需要确保这个自定义事件对应的原生事件被 React 的监听器捕获了。

ReactBrowserEventEmitter.js 中,你会看到 eventTypes 的定义。虽然大部分是 SimpleEventPlugin 定义的,但你需要确认你的插件是否正确地注册了 dependencies

如果你的 MyCustomEventPlugineventTypes 里定义了 dependencies: [topLevelTypes.topConfettiExplosion],那么 React 会自动在 document 上监听 topConfettiExplosion(也就是浏览器原生的 confettiexplosion 事件,注意浏览器通常不支持这种自定义事件,你需要用 dispatchEvent 手动触发)。

六、 事件池:内存的艺术

好了,现在你的插件已经写好了,注册表也改了。当你写代码的时候:

function App() {
  return (
    <div onConfettiExplosion={() => alert("Boom!")}>
      Click me to explode!
    </div>
  );
}

React 会怎么做?它会调用 MyCustomEventPlugin.extractEvents,创建一个合成事件对象,然后通过 EventPluginHubaccumulateTwoPhaseDispatches 方法,把这个事件分配给所有匹配的组件。

这里有一个非常重要的机制:Event Pooling(事件池)

为了性能,React 不会为每次点击都创建一个新的合成事件对象,它会从池子里拿出一个对象复用。

在你的 MyCustomEventPlugin.js 里,当你返回 new SyntheticEvent(...) 时,你需要注意这个对象的生命周期。React 在处理完事件后,会调用 event.reset() 把对象还原回池子。如果你在事件处理函数里保存了 event.target 的引用,而没有在 reset 之前把它拷贝出来,你的引用就会变成 null 或者指向错误的元素。

这是 React 源码中最容易踩坑的地方之一。

七、 总结:你需要改哪些文件?

好了,现在让我们把散落在各处的线索串起来。如果你想给 React 加一个自定义事件,你需要修改(或创建)以下文件:

  1. 创建插件文件: packages/react-dom/src/events/MyCustomEventPlugin.js

    • 实现 eventTypes
    • 实现 extractEvents
    • 实现插件的安装逻辑(如果需要)。
  2. 修改注册表(EventPluginRegistry): packages/react-dom/src/events/EventPluginRegistry.js

    • EventPluginOrder 数组中插入你的插件名字。
    • plugins 数组中导入并插入你的插件实例。
  3. 修改 SimpleEventPlugin(映射表): packages/react-dom/src/events/SimpleEventPlugin.js

    • topLevelEventsToReactNames 中添加映射(如果是映射原生事件)。
    • registrationNameModules 中添加映射(告诉 React 你的插件负责哪个事件名)。
  4. 修改 DOM 监听配置(可选): packages/react-dom/src/events/ReactBrowserEventEmitter.js

    • 确保你的插件依赖的原生事件被正确监听。

八、 进阶:EnterLeaveEventPlugin 的陷阱

如果你要加的不仅仅是普通事件,而是像 onMouseEnter 这样的逻辑事件,情况会更复杂。

MouseEnterMouseLeave 是 DOM 原生不支持的事件,它们依赖父级元素和子级元素的冒泡机制。React 的 EnterLeaveEventPlugin 就是专门处理这个“人造”事件的。

如果你要加一个类似的事件,比如 onConfettiEnter,你必须深入研究 EnterLeaveEventPluginextractEvents 方法。它利用了 SimpleEventPlugin 的结果,通过 EventPropagators 来判断元素是否真的进入了或离开了目标区域。

这就像是给 React 增加了一个全新的语法糖,你需要理解 React 如何利用“冒泡”这个特性来模拟“进入”和“离开”。

九、 另一种选择:ReactDOM.createPortal 与自定义事件

其实,还有一种更简单的方法,不需要动源码。你可以创建一个自定义的 Hook。

function useConfetti() {
  useEffect(() => {
    const handler = () => console.log("Boom!");
    document.addEventListener('confetti-explosion', handler);
    return () => document.removeEventListener('confetti-explosion', handler);
  }, []);
}

但是,这绕过了 React 的合成事件系统。这意味着你无法使用 e.stopPropagation(),也无法获得 React 合成事件的跨浏览器兼容性。对于追求极致性能和标准化的 React 项目来说,这是不可接受的。

十、 结语:源码中的“魔法”

写到这里,我们回顾一下。React 的事件系统之所以强大,是因为它把所有浏览器的不一致都封装了起来。它把复杂的 DOM 事件映射成了一个统一、干净的 SyntheticEvent

当你修改那些插件注册表时,你其实是在修改 React 的“宪法”。你把一个陌生的浏览器 API,翻译成了 React 专用的语言。这就是 React 框架的魅力所在——它不仅仅是一个库,它是一个精心设计的、有生命的系统。

如果你能熟练地在 SimpleEventPlugin 里加一行配置,在 EventPluginRegistry 里插一个队,你就能真正理解 React 是如何工作的。这不仅仅是修改代码,这是在理解计算机科学中最迷人的部分之一:抽象

好了,现在去你的项目里,试着给 React 加一个 onConfettiExplosion 吧。别把它搞炸了,但一定要搞懂它是怎么工作的。祝你好运,勇士!

发表回复

您的邮箱地址不会被公开。 必填项已用 * 标注