解析 React 的 ‘Event Plugin System’:如何为自定义渲染器实现一套跨平台的合成事件机制?

各位来宾,大家好!

今天,我们将深入探讨 React 核心机制之一——’Event Plugin System’,并在此基础上,共同探索如何为我们自己的自定义渲染器(Custom Renderer)构建一套同样强大且跨平台的合成事件机制。这是一个充满挑战但又极其有益的话题,它将揭示 React 能够如此灵活地在不同宿主环境(如 DOM、Native、Canvas 等)中运行的奥秘。

1. React 合成事件的必要性

在我们直接 diving into React 的事件系统之前,让我们先思考一个基本问题:为什么 React 不直接使用浏览器原生的 DOM 事件?

原因有以下几点:

  1. 跨浏览器兼容性: 不同的浏览器在原生事件的实现上存在差异,例如事件对象的属性、事件的冒泡/捕获行为、事件的默认行为处理等。直接使用原生事件会导致开发者需要处理大量的兼容性代码。
  2. 性能优化: 在大型应用中,为每个 DOM 元素都附加事件监听器会消耗大量内存和 CPU 资源。React 通过事件委托(Event Delegation)机制,在根节点上统一处理事件,显著提升了性能。
  3. 一致的 API: React 希望提供一套统一、简洁的事件处理 API,无论底层宿主环境是 DOM 还是其他(如 React Native 的 View),开发者都能以相同的方式编写事件处理逻辑。
  4. 事件池(Event Pooling): 虽然在 React 17 之后已被废弃,但在早期版本中,事件池用于复用事件对象,减少垃圾回收的压力,进一步优化性能。
  5. 生命周期管理: React 组件有其自身的生命周期。当组件挂载、更新或卸载时,其相关的事件监听器也需要被正确地添加或移除。合成事件系统与 React 的协调(Reconciliation)过程紧密结合,确保了事件处理与组件生命周期的一致性。

正是基于这些需求,React 引入了“合成事件”(Synthetic Events)的概念,它作为一层抽象,封装了原生事件的复杂性,提供了一个统一、高性能且易于使用的事件处理层。

2. React 事件插件系统的核心概念

React 的事件插件系统是一个高度模块化和可扩展的设计。它由几个关键概念组成:

2.1. 事件委托 (Event Delegation)

这是 React 事件系统最基础也是最重要的优化策略。React 不会将事件监听器直接附加到你 JSX 中定义的每个 DOM 元素上。相反,它会在应用的根节点(通常是 documentReactDOM.render 挂载的容器元素)上注册一次原生事件监听器。

当一个原生事件(例如 click)在任何子元素上触发并冒泡到根节点时,根节点上的监听器会捕获到这个事件。然后,React 会根据事件的 target 属性,向上遍历 React 组件树,找到所有相关的 React 事件处理器(如 onClick),并按照 React 定义的冒泡/捕获阶段依次触发它们。

2.2. 合成事件对象 (SyntheticEvent)

当原生事件被捕获后,React 会将其封装成一个 SyntheticEvent 对象。这个对象是一个跨浏览器的包装器,它提供了与 W3C 规范兼容的事件属性和方法。这意味着无论你使用的是 Chrome、Firefox 还是其他浏览器,event.stopPropagation()event.preventDefault()event.target 等行为和属性都会保持一致。

SyntheticEvent 对象通常包含以下核心属性和方法:

  • nativeEvent: 对底层原生事件的引用。
  • type: 事件类型(例如 ‘click’)。
  • target: 触发事件的 DOM 元素。
  • currentTarget: 当前正在处理事件的 React 元素(在冒泡/捕获阶段会变化)。
  • stopPropagation(): 阻止事件继续冒泡。
  • preventDefault(): 阻止事件的默认行为。
  • persist(): 阻止事件对象被放入池中(如果事件池启用),允许异步访问。
  • 其他特定事件类型(如 SyntheticMouseEventSyntheticKeyboardEvent)还会包含额外的属性,例如 clientX, clientY, keyCode, altKey 等。

2.3. 事件插件 (Event Plugins)

事件插件是 React 事件系统的核心扩展点。它们是负责处理特定类型事件的模块。每个插件都注册了一组它感兴趣的 React 事件名称(例如 onClickonMouseDown)以及它们对应的原生事件依赖(例如 clickmousedown)。

当原生事件发生时,事件系统会遍历所有注册的插件,让它们有机会“提取”(extract)出相关的合成事件对象,并将其放入一个待分发的队列中。

React 内部有多种事件插件,例如:

  • SimpleEventPlugin: 处理大多数常见的 DOM 事件,如 click, mousedown, input, change 等。
  • EnterLeaveEventPlugin: 处理 onMouseEnter, onMouseLeave 等特殊事件,它们不依赖于 DOM 原生的 mouseovermouseout 的冒泡行为。
  • ChangeEventPlugin: 专门处理表单元素的 onChange 事件,使其在不同浏览器和不同输入类型(input, select, textarea)下行为一致。
  • SelectEventPlugin: 处理 onSelect 事件。

2.4. 事件调度队列 (Dispatch Queue)

当一个原生事件被捕获并经过插件处理后,系统会构建一个事件调度队列。这个队列包含了所有需要被触发的 React 事件处理器及其对应的合成事件对象。队列会按照捕获阶段(从根到目标)和冒泡阶段(从目标到根)的顺序排列。

2.5. 事件注册与映射

React 内部维护了一个注册表,将 JSX 中的事件属性名(如 onClick)映射到其对应的原生事件类型(如 click)以及处理这些事件的插件。这个注册表是动态的,允许插件在初始化时注册其感兴趣的事件。

3. React 事件系统工作流 (简化版)

为了更好地理解,我们来概括一下 React 事件从原生到合成的简化流程:

  1. 应用启动/渲染:

    • ReactDOM.render()createRoot().render() 会在指定的根 DOM 节点(或 document)上注册少数几个最顶层的原生事件监听器(例如 click, keydown, input 等,这些都是由各个 EventPlugin 声明的依赖)。
    • 同时,React 会初始化并加载所有 EventPlugin
  2. 用户交互 (原生事件发生):

    • 用户在某个子 DOM 元素上执行操作,例如点击一个按钮。
    • 原生 click 事件从目标元素开始冒泡,直到被根节点上的监听器捕获。
  3. 事件分发器捕获:

    • 根节点上的原生事件监听器被触发。它接收到原生 MouseEvent 对象。
  4. 识别目标 React 实例:

    • React 会通过内部机制(例如,从原生 event.target DOM 元素向上查找,找到与之对应的 React Fiber 实例)来确定事件的“目标 React 实例”(targetInst)。
  5. 插件提取事件:

    • 事件系统遍历所有已注册的 EventPlugin
    • 每个插件的 extractEvents 方法被调用,传入原生事件、目标 React 实例等信息。
    • 如果插件识别出当前原生事件与它负责的某个 React 事件类型相关(例如 SimpleEventPlugin 识别出 click 事件需要生成 onClick 的合成事件),它就会创建一个或多个 SyntheticEvent 实例(例如 SyntheticMouseEvent)。
    • 这些合成事件连同其对应的处理器(通过遍历 React 实例的属性找到)被添加到事件调度队列中。
  6. 构建调度队列:

    • 事件系统根据 targetInstcurrentTargetInst 的关系,向上遍历 React Fiber 树,收集所有沿途的 onClick(捕获和冒泡)处理器,并将其与对应的 SyntheticEvent 对象打包成 { listener, event } 对,添加到调度队列中。
  7. 事件调度:

    • 事件系统按照捕获阶段(从根到目标)和冒泡阶段(从目标到根)的顺序,依次执行调度队列中的所有处理器。
    • 在执行过程中,SyntheticEvent 对象的 currentTarget 属性会动态更新,指向当前正在处理事件的 React 元素。
    • 如果某个处理器调用了 event.stopPropagation(),则会阻止后续的冒泡/捕获阶段的处理器被调用。
    • 如果调用了 event.preventDefault(),则会阻止原生事件的默认行为。
  8. 事件对象清理 (旧版本):

    • 在 React 17 之前,SyntheticEvent 对象会被放回事件池中以供复用。现在,事件对象会在事件处理完成后被销毁。

这个流程确保了 React 事件处理的统一性、高效性和可扩展性。

4. 为自定义渲染器实现跨平台合成事件机制

现在,我们面临真正的挑战:如何为我们自己的自定义渲染器(例如,一个基于 Canvas 或 WebGL 的渲染器,或者一个命令行界面渲染器)实现一套类似的跨平台合成事件机制?

假设我们正在构建一个名为 MyCustomRenderer 的渲染器,它不再操作 DOM 元素,而是操作我们自定义的“宿主元素”(MyHostElement),例如 Canvas 上的一个形状、一个 WebGL 对象或 CLI 上的一个文本块。这些 MyHostElement 不具备原生的 DOM 事件。

4.1. 核心挑战

  1. 没有原生 DOM 事件: 我们需要定义自己的“原生事件”或将外部输入(如鼠标坐标、键盘按键)转换为我们渲染器能够理解的“宿主原生事件”。
  2. 自定义宿主元素树: 我们需要维护一个类似于 DOM 树的“宿主元素树”,以便进行事件冒泡和捕获。
  3. 事件源: 如何从宿主环境(例如 Canvas 的 canvas.addEventListener)捕获原始输入,并将其桥接到我们的事件系统?
  4. React Fiber 实例映射: 如何从一个 MyHostElement 找到与之对应的 React Fiber 实例,以便查找其上的事件处理器?

4.2. 我们的自定义事件系统架构

为了解决这些挑战,我们将设计一个与 React 现有系统类似的架构,包含以下核心组件:

  1. MyNativeEvent 接口: 定义我们自定义渲染器中的“原生事件”结构。
  2. SyntheticEvent 基类及派生类: 我们的跨平台合成事件对象。
  3. EventPlugin 接口: 定义事件插件的契约。
  4. EventRegistry 注册 React 事件名称与原生事件类型的映射。
  5. EventDispatcher 负责从宿主环境接收 MyNativeEvent,通过插件提取合成事件,并调度给 React 组件。
  6. 宿主元素与 React 实例的映射: 解决 MyHostElement 到 React Fiber 实例的查找问题。

4.3. Step-by-Step 实现指南

Step 1: 定义宿主环境的“原生事件” (MyNativeEvent)

首先,我们需要抽象出我们的自定义渲染器所能接收的原始输入。例如,对于一个 Canvas 渲染器,这些可能是鼠标点击、移动、键盘按键等。

// src/events/MyNativeEvent.ts

/**
 * 我们的自定义宿主元素接口,需要有一个唯一的ID和父子关系,
 * 以便我们能够构建树结构并进行事件冒泡/捕获。
 */
interface MyHostElement {
  id: string; // 唯一标识符
  type: string; // 元素类型,例如 'shape', 'text', 'group'
  parent: MyHostElement | null;
  children: MyHostElement[];
  // 渲染器可能还需要其他属性,例如位置、大小、是否可见等
}

/**
 * 定义我们自定义渲染器的“原生事件”接口。
 * 它们是最低级别的输入事件,直接来自宿主环境(例如 Canvas 的原生事件监听器)。
 */
interface MyNativeEvent {
  type: string; // 例如 'click', 'mousedown', 'keydown'
  target: MyHostElement; // 触发事件的宿主元素
  // 鼠标事件特有属性
  clientX?: number;
  clientY?: number;
  offsetX?: number;
  offsetY?: number;
  button?: number;
  buttons?: number;
  // 键盘事件特有属性
  key?: string;
  keyCode?: number;
  altKey?: boolean;
  ctrlKey?: boolean;
  shiftKey?: boolean;
  metaKey?: boolean;

  // 阻止默认行为的方法,如果宿主环境有类似概念
  preventDefault?: () => void;
  // 阻止冒泡的方法,如果宿主环境有类似概念
  stopPropagation?: () => void;
}

export type { MyHostElement, MyNativeEvent };

Step 2: 创建 SyntheticEvent 基类及派生类

接下来,我们创建合成事件的基类。它将提供跨平台一致的 API,并包装我们的 MyNativeEvent

// src/events/SyntheticEvent.ts
import { MyNativeEvent, MyHostElement } from './MyNativeEvent';

/**
 * 合成事件基类。
 * 提供了跨平台一致的事件 API。
 */
class SyntheticEvent {
  nativeEvent: MyNativeEvent;
  type: string;
  target: MyHostElement;
  currentTarget: MyHostElement; // 在冒泡/捕获过程中会动态更新

  // 内部标志,用于阻止事件的传播和默认行为
  private _isPropagationStopped: boolean = false;
  private _isDefaultPrevented: boolean = false;

  constructor(
    reactEventType: string, // 例如 'onClick'
    nativeEvent: MyNativeEvent,
    target: MyHostElement,
    currentTarget: MyHostElement
  ) {
    this.type = reactEventType;
    this.nativeEvent = nativeEvent;
    this.target = target;
    this.currentTarget = currentTarget;
  }

  /**
   * 阻止事件继续冒泡或进入捕获阶段。
   */
  stopPropagation(): void {
    this._isPropagationStopped = true;
    if (this.nativeEvent.stopPropagation) {
      this.nativeEvent.stopPropagation();
    }
  }

  /**
   * 检查是否已调用 stopPropagation。
   */
  isPropagationStopped(): boolean {
    return this._isPropagationStopped;
  }

  /**
   * 阻止事件的默认行为。
   */
  preventDefault(): void {
    this._isDefaultPrevented = true;
    if (this.nativeEvent.preventDefault) {
      this.nativeEvent.preventDefault();
    }
  }

  /**
   * 检查是否已调用 preventDefault。
   */
  isDefaultPrevented(): boolean {
    return this._isDefaultPrevented;
  }

  /**
   * 在 React 17+ 中,事件池已废弃,此方法主要用于兼容性或作为空操作。
   * 在旧版本中,它用于阻止事件对象被回收到池中,允许异步访问。
   */
  persist(): void {
    // No-op for modern React behavior
  }
}

/**
 * 鼠标事件的合成事件类。
 */
class SyntheticMouseEvent extends SyntheticEvent {
  clientX: number;
  clientY: number;
  offsetX: number;
  offsetY: number;
  button: number;
  buttons: number;

  constructor(
    reactEventType: string,
    nativeEvent: MyNativeEvent,
    target: MyHostElement,
    currentTarget: MyHostElement
  ) {
    super(reactEventType, nativeEvent, target, currentTarget);
    this.clientX = nativeEvent.clientX || 0;
    this.clientY = nativeEvent.clientY || 0;
    this.offsetX = nativeEvent.offsetX || 0;
    this.offsetY = nativeEvent.offsetY || 0;
    this.button = nativeEvent.button || 0;
    this.buttons = nativeEvent.buttons || 0;
  }
}

/**
 * 键盘事件的合成事件类。
 */
class SyntheticKeyboardEvent extends SyntheticEvent {
  key: string;
  keyCode: number;
  altKey: boolean;
  ctrlKey: boolean;
  shiftKey: boolean;
  metaKey: boolean;

  constructor(
    reactEventType: string,
    nativeEvent: MyNativeEvent,
    target: MyHostElement,
    currentTarget: MyHostElement
  ) {
    super(reactEventType, nativeEvent, target, currentTarget);
    this.key = nativeEvent.key || '';
    this.keyCode = nativeEvent.keyCode || 0;
    this.altKey = nativeEvent.altKey || false;
    this.ctrlKey = nativeEvent.ctrlKey || false;
    this.shiftKey = nativeEvent.shiftKey || false;
    this.metaKey = nativeEvent.metaKey || false;
  }
}

// 导出所有合成事件类型
export { SyntheticEvent, SyntheticMouseEvent, SyntheticKeyboardEvent };

Step 3: 定义 EventPlugin 接口

事件插件是我们的扩展点。我们需要定义一个清晰的接口,让所有插件遵循。

// src/events/EventPlugin.ts
import { MyNativeEvent, MyHostElement } from './MyNativeEvent';
import { SyntheticEvent } from './SyntheticEvent';

/**
 * 定义事件分发队列中的项。
 * 包含事件监听器函数和对应的合成事件对象。
 */
interface DispatchEntry {
  listener: Function;
  event: SyntheticEvent;
}

/**
 * React 事件类型注册信息。
 * phasedRegistrationNames 包含捕获和冒泡阶段的 React 事件属性名。
 */
interface EventType {
  phasedRegistrationNames: {
    bubbled: string; // 例如 'onClick'
    captured: string; // 例如 'onClickCapture'
  };
  dependencies: string[]; // 依赖的原生事件类型,例如 ['click', 'mousedown']
}

/**
 * 事件插件接口。
 * 所有自定义事件插件都需要实现这个接口。
 */
interface EventPlugin {
  /**
   * 定义插件所关心的 React 事件类型。
   * 键是 React 事件名(如 'onClick'),值是 EventType 对象。
   */
  eventTypes: { [key: string]: EventType };

  /**
   * 定义 React 事件属性名到原生事件依赖的映射。
   * 例如:`onClick` -> `click`
   */
  registrationNameDependencies: { [key: string]: string[] };

  /**
   * 从原生事件中提取合成事件并填充到调度队列中。
   * @param dispatchQueue - 待填充的事件调度队列。
   * @param reactEventType - 目标 React 事件类型 (例如 'onClick')。
   * @param nativeEvent - 原始的宿主原生事件。
   * @param targetInst - 触发事件的 React Fiber 实例。
   * @param currentTargetInst - 当前正在处理事件的 React Fiber 实例(在遍历过程中会变化)。
   * @param getListeners - 用于根据 React 实例和事件类型获取监听器的方法。
   */
  extractEvents(
    dispatchQueue: DispatchEntry[],
    reactEventType: string,
    nativeEvent: MyNativeEvent,
    targetInst: any, // 对应 React Fiber 实例
    currentTargetInst: any, // 对应 React Fiber 实例
    getListeners: (inst: any, reactEventType: string) => Function[]
  ): void;
}

export type { EventPlugin, EventType, DispatchEntry };

Step 4: 实现一个基本的 SimpleEventPlugin

这个插件将处理我们最常见的鼠标和键盘事件。

// src/events/SimpleEventPlugin.ts
import { EventPlugin, EventType, DispatchEntry } from './EventPlugin';
import { MyNativeEvent, MyHostElement } from './MyNativeEvent';
import { SyntheticMouseEvent, SyntheticKeyboardEvent } from './SyntheticEvent';

/**
 * 一个简单的事件插件,处理常见的鼠标和键盘事件。
 */
class SimpleEventPlugin implements EventPlugin {
  eventTypes: { [key: string]: EventType } = {
    onClick: {
      phasedRegistrationNames: {
        bubbled: 'onClick',
        captured: 'onClickCapture',
      },
      dependencies: ['click'],
    },
    onMouseDown: {
      phasedRegistrationNames: {
        bubbled: 'onMouseDown',
        captured: 'onMouseDownCapture',
      },
      dependencies: ['mousedown'],
    },
    onMouseUp: {
      phasedRegistrationNames: {
        bubbled: 'onMouseUp',
        captured: 'onMouseUpCapture',
      },
      dependencies: ['mouseup'],
    },
    onKeyDown: {
      phasedRegistrationNames: {
        bubbled: 'onKeyDown',
        captured: 'onKeyDownCapture',
      },
      dependencies: ['keydown'],
    },
    onKeyUp: {
      phasedRegistrationNames: {
        bubbled: 'onKeyUp',
        captured: 'onKeyUpCapture',
      },
      dependencies: ['keyup'],
    },
    // ... 可以添加更多事件类型
  };

  // 映射 React 事件属性名到它依赖的原生事件类型
  registrationNameDependencies: { [key: string]: string[] } = {
    onClick: ['click'],
    onClickCapture: ['click'],
    onMouseDown: ['mousedown'],
    onMouseDownCapture: ['mousedown'],
    onMouseUp: ['mouseup'],
    onMouseUpCapture: ['mouseup'],
    onKeyDown: ['keydown'],
    onKeyDownCapture: ['keydown'],
    onKeyUp: ['keyup'],
    onKeyUpCapture: ['keyup'],
  };

  extractEvents(
    dispatchQueue: DispatchEntry[],
    reactEventType: string,
    nativeEvent: MyNativeEvent,
    targetInst: any,
    currentTargetInst: any,
    getListeners: (inst: any, reactEventType: string) => Function[]
  ): void {
    const { nativeEvent: baseNativeEvent, target: baseTarget, currentTarget: baseCurrentTarget } = nativeEvent;

    let SyntheticEventClass: typeof SyntheticMouseEvent | typeof SyntheticKeyboardEvent | typeof SyntheticEvent = SyntheticEvent;

    // 根据原生事件类型选择合适的合成事件类
    if (nativeEvent.type.startsWith('mouse')) {
      SyntheticEventClass = SyntheticMouseEvent;
    } else if (nativeEvent.type.startsWith('key')) {
      SyntheticEventClass = SyntheticKeyboardEvent;
    }

    const event = new SyntheticEventClass(
      reactEventType,
      nativeEvent,
      targetInst.stateNode, // targetInst.stateNode 应该是 MyHostElement
      currentTargetInst.stateNode // currentTargetInst.stateNode 应该是 MyHostElement
    );

    // 获取捕获阶段的监听器
    const captureListeners = getListeners(currentTargetInst, this.eventTypes[reactEventType].phasedRegistrationNames.captured);
    captureListeners.forEach(listener => {
      dispatchQueue.push({ listener, event });
    });

    // 获取冒泡阶段的监听器
    const bubbleListeners = getListeners(currentTargetInst, this.eventTypes[reactEventType].phasedRegistrationNames.bubbled);
    bubbleListeners.forEach(listener => {
      dispatchQueue.push({ listener, event });
    });
  }
}

export { SimpleEventPlugin };

Step 5: 开发 EventDispatcher

这是我们事件系统的核心,负责接收所有宿主原生事件,并将它们转换为 React 合成事件并分发。

// src/events/EventDispatcher.ts
import { MyNativeEvent, MyHostElement } from './MyNativeEvent';
import { EventPlugin, DispatchEntry, EventType } from './EventPlugin';
import { SimpleEventPlugin } from './SimpleEventPlugin';
import { SyntheticEvent } from './SyntheticEvent';

/**
 * EventDispatcher 是我们自定义渲染器事件系统的核心。
 * 它负责:
 * 1. 注册和管理事件插件。
 * 2. 接收宿主环境的原始 MyNativeEvent。
 * 3. 将 MyNativeEvent 转换为 SyntheticEvent。
 * 4. 调度 SyntheticEvent 给正确的 React 组件监听器。
 */
class EventDispatcher {
  private plugins: EventPlugin[] = [];
  // 映射原生事件类型到关心它的插件
  private nativeEventToPluginsMap: Map<string, EventPlugin[]> = new Map();
  // 映射 React 事件名到原生事件类型
  private reactEventToNativeEventMap: Map<string, string[]> = new Map();
  // 映射 React 事件名到其 EventType 定义
  private reactEventTypeDefinitions: Map<string, EventType> = new Map();

  // 渲染器需要提供一个方法,将 MyHostElement 映射回 React Fiber 实例
  private getInstanceFromNode: (node: MyHostElement) => any;
  // 渲染器需要提供一个方法,从 React Fiber 实例获取其上的事件监听器
  private getListenersFromInstance: (inst: any, reactPropName: string) => Function[];

  constructor(
    getInstanceFromNode: (node: MyHostElement) => any,
    getListenersFromInstance: (inst: any, reactPropName: string) => Function[]
  ) {
    this.getInstanceFromNode = getInstanceFromNode;
    this.getListenersFromInstance = getListenersFromInstance;
    this.registerPlugin(new SimpleEventPlugin()); // 注册默认插件
  }

  /**
   * 注册一个事件插件。
   * @param plugin 要注册的事件插件实例。
   */
  registerPlugin(plugin: EventPlugin): void {
    this.plugins.push(plugin);

    // 填充 nativeEventToPluginsMap 和 reactEventToNativeEventMap
    for (const reactEventType in plugin.eventTypes) {
      if (plugin.eventTypes.hasOwnProperty(reactEventType)) {
        const eventTypeDefinition = plugin.eventTypes[reactEventType];
        this.reactEventTypeDefinitions.set(reactEventType, eventTypeDefinition);

        for (const nativeEventType of eventTypeDefinition.dependencies) {
          if (!this.nativeEventToPluginsMap.has(nativeEventType)) {
            this.nativeEventToPluginsMap.set(nativeEventType, []);
          }
          this.nativeEventToPluginsMap.get(nativeEventType)!.push(plugin);
        }
        if (!this.reactEventToNativeEventMap.has(reactEventType)) {
          this.reactEventToNativeEventMap.set(reactEventType, []);
        }
        this.reactEventToNativeEventMap.get(reactEventType)!.push(...eventTypeDefinition.dependencies);
      }
    }
  }

  /**
   * 接收来自宿主环境的原始 MyNativeEvent,并开始分发流程。
   * 这是我们的事件系统入口点。
   * @param nativeEvent 原始的宿主原生事件。
   */
  handleEvent(nativeEvent: MyNativeEvent): void {
    const targetNode = nativeEvent.target;
    if (!targetNode) {
      return; // 没有目标元素,无法处理
    }

    // 1. 找到目标 React Fiber 实例
    const targetInst = this.getInstanceFromNode(targetNode);
    if (!targetInst) {
      return; // 没有对应的 React 实例
    }

    // 2. 遍历所有相关的插件,提取合成事件
    const dispatchQueue: DispatchEntry[] = [];
    const pluginsForNativeEvent = this.nativeEventToPluginsMap.get(nativeEvent.type) || [];

    // 获取从目标元素到根元素的路径 (MyHostElement 数组)
    const path = this.getPathFromTargetToRoot(targetNode);

    // 遍历路径,模拟冒泡和捕获阶段的 currentTarget 变化
    // 注意:这里的 extractEvents 通常只在目标元素上触发一次,
    // 但我们会为每个路径上的元素生成事件对象,并设置正确的 currentTarget。
    // 这与 React 内部的实现有所简化,React 通常是先提取事件,再根据事件对象分发。
    // 为了简化,我们在这里直接构建完整的调度队列。

    for (const reactEventType of this.reactEventTypeDefinitions.keys()) {
      const eventDef = this.reactEventTypeDefinitions.get(reactEventType)!;
      if (eventDef.dependencies.includes(nativeEvent.type)) {
        // 模拟捕获阶段
        for (let i = path.length - 1; i >= 0; i--) {
          const currentTargetNode = path[i];
          const currentTargetInst = this.getInstanceFromNode(currentTargetNode);
          if (currentTargetInst) {
            // 在这里调用插件提取事件,并传递正确的 currentTargetInst
            for (const plugin of pluginsForNativeEvent) {
              plugin.extractEvents(
                dispatchQueue,
                reactEventType,
                nativeEvent,
                targetInst, // targetInst 始终是原始目标
                currentTargetInst, // currentTargetInst 随着遍历路径而变化
                this.getListenersFromInstance // 传递获取监听器的方法
              );
            }
          }
        }

        // 模拟冒泡阶段 (通常在 extractEvents 内部处理,这里简化)
        // 实际上, extractEvents 应该一次性生成所有捕获和冒泡的 dispatch entries
        // 这里只是一个简化示例,假定 extractEvents 能处理不同 currentTargetInst 的情况
      }
    }

    // 3. 排序调度队列 (先捕获,后冒泡)
    // 实际 React 的 dispatchQueue 是按照捕获 -> 目标 -> 冒泡的顺序排列的。
    // 并且同一个合成事件对象会在整个过程中被复用,只有 currentTarget 变化。
    // 我们的 SimpleEventPlugin 每次提取都创建了新的 SyntheticEvent,
    // 因此需要更精细的调度逻辑来确保 currentTarget 的正确性和 stopPropagation 的工作。
    // 为了更接近 React 行为,extractEvents 应该只创建一次 SyntheticEvent,
    // 然后将该事件对象与不同 currentTarget 的监听器组合放入队列。

    // 这是一个更接近 React 实际行为的调度逻辑:
    const finalDispatchQueue: DispatchEntry[] = [];
    const eventTypeDefinitions = this.reactEventTypeDefinitions;

    // 遍历所有可能的 React 事件类型 (例如 'onClick', 'onMouseDown')
    for (const reactEventType of eventTypeDefinitions.keys()) {
      const eventDef = eventTypeDefinitions.get(reactEventType)!;
      if (!eventDef.dependencies.includes(nativeEvent.type)) {
        continue; // 此 React 事件类型不依赖于当前的原生事件
      }

      // 创建一个合成事件对象。这个对象将在整个捕获/冒泡过程中复用。
      // 注意:这里我们假设 SimpleEventPlugin 能够创建正确的 SyntheticEvent 实例
      // 并且我们只需要一个,而不是每次都创建新的。
      // 这里的 `SyntheticEvent` 类应该能够根据 `reactEventType` 的类型派生。
      // 为了简化,我们直接使用 SyntheticMouseEvent/KeyboardEvent,实际应更灵活。

      const tempSyntheticEvent = nativeEvent.type.startsWith('mouse') ?
        new SyntheticMouseEvent(reactEventType, nativeEvent, targetNode, targetNode) :
        nativeEvent.type.startsWith('key') ?
        new SyntheticKeyboardEvent(reactEventType, nativeEvent, targetNode, targetNode) :
        new SyntheticEvent(reactEventType, nativeEvent, targetNode, targetNode);

      // 获取从目标元素到根元素的路径 (React Fiber 实例数组)
      const pathInsts: any[] = [];
      let currentInst = targetInst;
      while (currentInst) {
        pathInsts.push(currentInst);
        currentInst = currentInst.return; // 假设 Fiber 实例有 return 属性指向父级
      }
      pathInsts.reverse(); // 根到目标

      // 捕获阶段 (从根到目标)
      for (let i = 0; i < pathInsts.length; i++) {
        const currentTargetInst = pathInsts[i];
        tempSyntheticEvent.currentTarget = currentTargetInst.stateNode; // 更新 currentTarget

        const captureListeners = this.getListenersFromInstance(
          currentTargetInst,
          eventDef.phasedRegistrationNames.captured
        );
        captureListeners.forEach(listener => {
          finalDispatchQueue.push({ listener, event: tempSyntheticEvent });
        });
        if (tempSyntheticEvent.isPropagationStopped()) {
          break; // 如果在捕获阶段停止传播,则中断
        }
      }

      // 冒泡阶段 (从目标到根)
      for (let i = pathInsts.length - 1; i >= 0; i--) {
        if (tempSyntheticEvent.isPropagationStopped()) {
          break; // 如果在冒泡阶段停止传播,则中断
        }
        const currentTargetInst = pathInsts[i];
        tempSyntheticEvent.currentTarget = currentTargetInst.stateNode; // 更新 currentTarget

        const bubbleListeners = this.getListenersFromInstance(
          currentTargetInst,
          eventDef.phasedRegistrationNames.bubbled
        );
        bubbleListeners.forEach(listener => {
          finalDispatchQueue.push({ listener, event: tempSyntheticEvent });
        });
      }
    }

    // 4. 执行调度队列
    for (const { listener, event } of finalDispatchQueue) {
      if (event.isPropagationStopped()) {
        break; // 如果事件在某个处理函数中停止了传播,则停止后续分发
      }
      try {
        listener(event);
      } catch (error) {
        console.error('Error in event listener:', error);
        // 这里可以实现 React 的错误边界机制
      }
    }

    // 5. 处理默认行为
    if (tempSyntheticEvent && tempSyntheticEvent.isDefaultPrevented()) {
      // 如果合成事件阻止了默认行为,那么我们也阻止原生事件的默认行为
      if (nativeEvent.preventDefault) {
        nativeEvent.preventDefault();
      }
    }
  }

  /**
   * 辅助函数:从目标宿主元素向上遍历到根元素,构建路径。
   * @param targetNode 目标宿主元素。
   * @returns 从根到目标元素的 MyHostElement 数组。
   */
  private getPathFromTargetToRoot(targetNode: MyHostElement): MyHostElement[] {
    const path: MyHostElement[] = [];
    let current: MyHostElement | null = targetNode;
    while (current) {
      path.push(current);
      current = current.parent;
    }
    return path.reverse(); // 返回从根到目标的路径
  }
}

export { EventDispatcher };

表格:React 事件名与原生事件依赖映射示例

React 事件名 捕获阶段属性名 冒泡阶段属性名 依赖的原生事件类型 对应插件
onClick onClickCapture onClick click SimpleEventPlugin
onMouseDown onMouseDownCapture onMouseDown mousedown SimpleEventPlugin
onMouseUp onMouseUpCapture onMouseUp mouseup SimpleEventPlugin
onKeyDown onKeyDownCapture onKeyDown keydown SimpleEventPlugin
onKeyUp onKeyUpCapture onKeyUp onKeyUp SimpleEventPlugin
onChange onChangeCapture onChange input, change ChangeEventPlugin
onMouseEnter N/A onMouseEnter mouseover, mouseout (特殊处理) EnterLeaveEventPlugin

Step 6: 与你的自定义 Reconciler 集成

这是最关键的一步,它将我们的事件系统与 React 的协调器 (react-reconciler) 框架连接起来。

当你使用 react-reconciler 创建自定义渲染器时,你需要实现一个 HostConfig 对象。这个 HostConfig 包含了 React 协调器与你的宿主环境交互所需的所有方法。

我们需要在 HostConfig 中做以下几件事:

  1. 存储事件监听器: 当 React 创建或更新一个宿主元素时,它会提供 props。我们需要将 onClickonMouseDown 等事件处理器从 props 中提取出来,并存储在宿主元素或 React Fiber 实例的某个地方,以便 EventDispatcher 能够检索到它们。

    • createInstance(type, props, ...)commitUpdate(instance, updatePayload, ...) 方法中处理。
    • 通常,我们会将这些监听器存储在 React Fiber 实例的 memoizedPropsstateNode(即 MyHostElement)的某个内部属性上。
  2. 提供 getInstanceFromNodegetListenersFromInstance EventDispatcher 依赖这两个方法来将 MyHostElement 映射回 React Fiber 实例,并从 Fiber 实例中获取事件监听器。

    • getInstanceFromNode(node: MyHostElement): 这需要你在 createInstance 时建立一个从 MyHostElement 到 React Fiber 实例的映射(例如,一个 WeakMap)。
    • getListenersFromInstance(inst: any, reactPropName: string): 这个方法会根据 reactPropName(如 onClick)从 inst.memoizedProps 中获取对应的函数。
  3. 在根节点附加 EventDispatcher 当 React 渲染器挂载到根容器时,我们需要实例化 EventDispatcher 并将其与宿主环境的原始输入事件源连接起来。

    • prepareForCommit(containerInfo) 中,你可以初始化 EventDispatcher
    • resetAfterCommit(containerInfo) 中,你可以清理或执行其他后处理。
    • 关键在于,你的宿主环境(例如 Canvas 容器)需要有一个机制来捕获原始输入(如 canvas.addEventListener('click', ...)),并将这些原始输入转换为 MyNativeEvent,然后传递给 eventDispatcher.handleEvent(myNativeEvent)

简化的 HostConfig 示例片段:

// src/reconciler/HostConfig.ts
import * as Reconciler from 'react-reconciler';
import { MyHostElement, MyNativeEvent } from '../events/MyNativeEvent';
import { EventDispatcher } from '../events/EventDispatcher';

// 假设我们有一个全局的映射来存储 MyHostElement 到 Fiber 实例的关系
const HostElementToFiberMap = new WeakMap<MyHostElement, Reconciler.Fiber>();

// 全局的 EventDispatcher 实例
let eventDispatcher: EventDispatcher | null = null;

const HostConfig: Reconciler.HostConfig<
  string, // Type: 宿主元素类型,例如 'shape', 'text'
  {}, // Props: 宿主元素的属性
  MyHostElement, // Container: 根容器元素,也可能是 MyHostElement
  MyHostElement, // Instance: 宿主元素实例
  any, // TextInstance: 文本元素实例
  any, // SuspenseInstance
  any, // HydratableInstance
  any, // PublicInstance
  any, // HostContext
  any, // UpdatePayload
  any, // ChildSet
  any, // TimeoutHandle
  any // NoTimeout
> = {
  // ... 其他 HostConfig 方法 ...

  // ---------------- 事件系统相关 ----------------
  // 用于获取 React Fiber 实例的辅助函数
  getInstanceFromNode(node: MyHostElement): Reconciler.Fiber | null {
    return HostElementToFiberMap.get(node) || null;
  },

  // 用于从 React Fiber 实例获取事件监听器的辅助函数
  getListenersFromInstance(inst: Reconciler.Fiber, reactPropName: string): Function[] {
    const listeners: Function[] = [];
    if (inst && inst.memoizedProps && typeof inst.memoizedProps[reactPropName] === 'function') {
      listeners.push(inst.memoizedProps[reactPropName]);
    }
    return listeners;
  },

  // 创建宿主元素实例
  createInstance(
    type: string,
    props: {},
    rootContainerInstance: MyHostElement,
    hostContext: any,
    internalInstanceHandle: Reconciler.Fiber // React Fiber 实例
  ): MyHostElement {
    const instance: MyHostElement = {
      id: Math.random().toString(36).substring(7), // 简化ID生成
      type: type,
      parent: null, // 稍后在 appendChild 中设置
      children: [],
      // ... 其他宿主元素特有属性 ...
    };

    // 建立 MyHostElement 到 Fiber 实例的映射
    HostElementToFiberMap.set(instance, internalInstanceHandle);

    // 将事件监听器直接存储在 Fiber 实例的 props 中 (Reconciler 会处理)
    // 或者你可以选择存储在 MyHostElement 上

    return instance;
  },

  // 更新宿主元素实例
  commitUpdate(
    instance: MyHostElement,
    updatePayload: any, // 包含变化的 props
    type: string,
    oldProps: {},
    newProps: Reconciler.InstanceProps,
    internalInstanceHandle: Reconciler.Fiber
  ): void {
    // 处理属性更新,特别是事件监听器的更新
    // Reconciler 内部会更新 internalInstanceHandle.memoizedProps
    // 所以我们的 getListenersFromInstance 会自动获取最新的监听器
  },

  // 在提交前准备(例如,初始化事件系统)
  prepareForCommit(containerInfo: MyHostElement): Record<string, any> | null {
    if (!eventDispatcher) {
      eventDispatcher = new EventDispatcher(
        HostConfig.getInstanceFromNode,
        HostConfig.getListenersFromInstance
      );

      // 这里需要将宿主环境的原始事件源连接到 eventDispatcher
      // 假设 containerInfo 是我们的 Canvas 根元素
      // 并且它有一个方法来注册原生事件处理
      if (typeof (containerInfo as any)._registerNativeEventHandler === 'function') {
         (containerInfo as any)._registerNativeEventHandler((nativeEvent: MyNativeEvent) => {
            if (eventDispatcher) {
                eventDispatcher.handleEvent(nativeEvent);
            }
         });
      }
    }
    return null;
  },

  // 提交后清理(例如,移除事件监听器)
  resetAfterCommit(containerInfo: MyHostElement): void {
    // 可以在这里进行一些清理工作,但对于事件系统,通常不需要在每次提交后重置。
  },

  // ... 其他生命周期方法,例如 appendChild, removeChild, etc.
  appendChild(parentInstance: MyHostElement, child: MyHostElement): void {
    parentInstance.children.push(child);
    child.parent = parentInstance;
  },

  insertBefore(parentInstance: MyHostElement, child: MyHostElement, beforeChild: MyHostElement): void {
      const index = parentInstance.children.indexOf(beforeChild);
      if (index > -1) {
          parentInstance.children.splice(index, 0, child);
          child.parent = parentInstance;
      }
  },

  removeChild(parentInstance: MyHostElement, child: MyHostElement): void {
      const index = parentInstance.children.indexOf(child);
      if (index > -1) {
          parentInstance.children.splice(index, 1);
          child.parent = null; // 解除父子关系
      }
  },

  // ... 更多 Reconciler.HostConfig 必选方法
  supportsMutation: true,
  supportsPersistence: false,
  supportsHydration: false,

  // Text 节点处理
  createTextInstance(
      text: string,
      rootContainerInstance: MyHostElement,
      hostContext: any,
      internalInstanceHandle: Reconciler.Fiber
  ): any {
      // 文本实例的实现取决于你的渲染器
      return { text, type: 'text', parent: null };
  },

  appendInitialChild(parentInstance: MyHostElement, child: MyHostElement): void {
      parentInstance.children.push(child);
      child.parent = parentInstance;
  },

  appendAllChildren(parentInstance: MyHostElement, newChildren: any[]): void {
      newChildren.forEach(child => this.appendChild(parentInstance, child));
  },

  finalizeInitialChildren(
      instance: MyHostElement,
      type: string,
      props: {},
      rootContainerInstance: MyHostElement,
      hostContext: any,
  ): boolean {
      return false; // 通常用于 DOM 元素自动聚焦等副作用
  },

  getChildHostContext(parentHostContext: any, type: string, rootContainerInstance: MyHostElement): any {
      return parentHostContext;
  },

  getPublicInstance(instance: MyHostElement): any {
      return instance; // 返回宿主元素的公共接口
  },

  getRootHostContext(rootContainerInstance: MyHostElement): any {
      return {};
  },

  prepareUpdate(
      instance: MyHostElement,
      type: string,
      oldProps: {},
      newProps: {},
      rootContainerInstance: MyHostElement,
      hostContext: any,
  ): any {
      const updatePayload = {};
      // 比较 oldProps 和 newProps,生成更新 Payload
      // 例如,如果 'onClick' 变化了,则 Payload 会包含这些信息
      return updatePayload; // 如果没有更新,返回 null
  },

  shouldSetTextContent(type: string, props: {}): boolean {
      return false;
  },

  createContainerChildSet(container: MyHostElement): any {
      return []; // 用于批量更新
  },

  appendChildToContainer(container: MyHostElement, child: MyHostElement): void {
      container.children.push(child);
      child.parent = container;
  },

  insertInContainerBefore(container: MyHostElement, child: MyHostElement, beforeChild: MyHostElement): void {
      const index = container.children.indexOf(beforeChild);
      if (index > -1) {
          container.children.splice(index, 0, child);
          child.parent = container;
      }
  },

  removeChildFromContainer(container: MyHostElement, child: MyHostElement): void {
      const index = container.children.indexOf(child);
      if (index > -1) {
          container.children.splice(index, 1);
          child.parent = null;
      }
  },

  commitTextUpdate(textInstance: any, oldText: string, newText: string): void {
      textInstance.text = newText;
  },

  clearContainer(container: MyHostElement): void {
      container.children = [];
  },

  hideInstance(instance: MyHostElement): void {
    // 隐藏逻辑
  },
  unhideInstance(instance: MyHostElement, props: any): void {
    // 显示逻辑
  },
  hideTextInstance(textInstance: any): void {
    // 隐藏文本逻辑
  },
  unhideTextInstance(textInstance: any, props: any): void {
    // 显示文本逻辑
  },
};

export default HostConfig;

模拟宿主环境的事件源:

为了让 EventDispatcher 工作,你的自定义渲染器的根容器(例如,一个 Canvas 封装类)需要能够捕获底层的原始输入事件,并将其转换为 MyNativeEvent 传递给 EventDispatcher

// src/renderer/MyCanvasRoot.ts
import { MyHostElement, MyNativeEvent } from '../events/MyNativeEvent';

// 这是一个模拟的 Canvas 宿主根容器
class MyCanvasRoot implements MyHostElement {
  id: string = 'root';
  type: string = 'canvasRoot';
  parent: MyHostElement | null = null;
  children: MyHostElement[] = [];
  private canvas: HTMLCanvasElement;
  private nativeEventHandler: ((event: MyNativeEvent) => void) | null = null;

  constructor(canvasElement: HTMLCanvasElement) {
    this.canvas = canvasElement;
    this.setupNativeListeners();
  }

  // 假设渲染器有自己的渲染逻辑来找到点击的 MyHostElement
  private findTargetElement(x: number, y: number): MyHostElement {
    // 实际渲染器中,这里会根据 x, y 坐标和 children 数组的渲染区域进行碰撞检测
    // 为了简化,我们假设总是点击到 root 自身
    return this;
  }

  private setupNativeListeners(): void {
    this.canvas.addEventListener('click', (e: MouseEvent) => {
      const target = this.findTargetElement(e.offsetX, e.offsetY);
      const myNativeEvent: MyNativeEvent = {
        type: 'click',
        target: target,
        clientX: e.clientX,
        clientY: e.clientY,
        offsetX: e.offsetX,
        offsetY: e.offsetY,
        button: e.button,
        buttons: e.buttons,
        preventDefault: () => e.preventDefault(),
        stopPropagation: () => e.stopPropagation(),
      };
      if (this.nativeEventHandler) {
        this.nativeEventHandler(myNativeEvent);
      }
    });

    this.canvas.addEventListener('mousedown', (e: MouseEvent) => {
        const target = this.findTargetElement(e.offsetX, e.offsetY);
        const myNativeEvent: MyNativeEvent = {
            type: 'mousedown',
            target: target,
            clientX: e.clientX,
            clientY: e.clientY,
            offsetX: e.offsetX,
            offsetY: e.offsetY,
            button: e.button,
            buttons: e.buttons,
            preventDefault: () => e.preventDefault(),
            stopPropagation: () => e.stopPropagation(),
        };
        if (this.nativeEventHandler) {
            this.nativeEventHandler(myNativeEvent);
        }
    });

    // ... 注册其他原生事件,如 mouseup, keydown, keyup 等
  }

  // 这个方法供 HostConfig 调用,以注册 EventDispatcher 的 handleEvent 方法
  _registerNativeEventHandler(handler: (event: MyNativeEvent) => void): void {
    this.nativeEventHandler = handler;
  }

  // 渲染方法 (这里只是一个占位符)
  render(): void {
    const ctx = this.canvas.getContext('2d');
    if (ctx) {
      ctx.clearRect(0, 0, this.canvas.width, this.canvas.height);
      // 遍历 children 并渲染
      this.children.forEach(child => {
        // 假设每个 MyHostElement 都有一个 render 方法
        // (child as any).render(ctx);
      });
      ctx.fillStyle = 'blue';
      ctx.fillRect(0,0,100,100); // 绘制一个示例矩形
    }
  }
}

export default MyCanvasRoot;

最后,你的自定义渲染器入口文件可能会像这样:

// src/renderer/index.ts
import * as Reconciler from 'react-reconciler';
import HostConfig from './reconciler/HostConfig';
import MyCanvasRoot from './renderer/MyCanvasRoot';

const MyCustomReconciler = Reconciler.default(HostConfig);

export function createRenderer(canvasElement: HTMLCanvasElement) {
  const rootContainer = new MyCanvasRoot(canvasElement);
  const reactRoot = MyCustomReconciler.createContainer(
    rootContainer,
    0, // Concurrent mode
    null, // hydration
    false, // is
Hydrating
    null, // onRecoverableError
    '', // identifierPrefix
    null, // onAttemptSyncBlock
    null // onSet</li>ImmutableReadonly
  );

  return {
    render(element: React.ReactNode, callback?: () => void) {
      MyCustomReconciler.updateContainer(element, reactRoot, null, callback);
      rootContainer.render(); // 每次更新后重新渲染 Canvas
    },
    unmount(callback?: () => void) {
      MyCustomReconciler.updateContainer(null, reactRoot, null, callback);
    }
  };
}
// example/App.tsx
import React, { useEffect, useRef } from 'react';
import { createRenderer } from '../src/renderer'; // 假设这是你的入口

interface MyCanvasElementProps {
  onClick?: (event: any) => void;
  // ... 其他自定义属性
}

declare global {
  namespace JSX {
    interface IntrinsicElements {
      'my-shape': MyCanvasElementProps; // 声明自定义宿主元素
    }
  }
}

function MyComponent() {
  const handleClick = (e: any) => {
    console.log('MyComponent clicked!', e.type, e.target.id);
    e.stopPropagation(); // 阻止事件冒泡
  };

  return (
    <my-shape onClick={handleClick} />
  );
}

function App() {
  const canvasRef = useRef<HTMLCanvasElement>(null);

  useEffect(() => {
    if (canvasRef.current) {
      const { render } = createRenderer(canvasRef.current);
      render(<MyComponent />);
    }
  }, []);

  return (
    <div>
      <h1>My Custom React Renderer</h1>
      <canvas ref={canvasRef} width="400" height="300" style={{ border: '1px solid black' }} />
    </div>
  );
}

export default App;

5. 高级考量与优化

5.1. 错误处理与错误边界

React 的事件系统与错误边界 (Error Boundaries) 紧密集成。当事件监听器抛出错误时,错误会被捕获并传播到最近的错误边界。在我们的自定义系统中,EventDispatcherhandleEvent 方法中的 try...catch 块是一个起点,但要完全模拟 React 的行为,需要将错误传递给 react-reconciler 提供的 onRecoverableError 或其他内部错误处理机制。

5.2. 性能优化:批处理与优先级

React 18 引入了自动批处理(Automatic Batching)和并发模式(Concurrent Mode),极大地优化了性能。事件处理是触发批处理的关键点。在我们的 EventDispatcher 中,handleEvent 内部的 for...of finalDispatchQueue 循环执行监听器时,可以将其包裹在 ReactDOM.unstable_batchedUpdates (或 React 内部等效的调度器) 中,以确保在事件处理周期内多次 setState 调用被合并为一次渲染。

5.3. 自定义事件类型

除了模仿 DOM 事件,你可能还需要为你的渲染器定义完全自定义的事件,例如 onDragStartonZoom 等。你可以通过创建新的 EventPlugin 来实现这一点,这些插件可以监听更低级别的 MyNativeEvent 组合(如 mousedown + mousemove),然后合成并分发这些高级事件。

5.4. currentTarget 的正确性

在事件冒泡和捕获过程中,event.currentTarget 应该指向当前正在执行监听器的 React 元素所对应的宿主元素。我们的 EventDispatcher 通过在遍历 pathInsts 时动态更新 tempSyntheticEvent.currentTarget 来模拟这一行为。

5.5. 内存管理与垃圾回收

虽然 React 17 废弃了事件池,但对于高性能的自定义渲染器,如果事件创建非常频繁且开销大,重新考虑某种形式的事件对象池仍然可能是有益的。然而,现代 JavaScript 引擎的垃圾回收器效率很高,通常在没有明确池化的情况下也能表现良好。

5.6. 宿主元素树的遍历效率

getPathFromTargetToRoot 方法在每次事件发生时都会遍历宿主元素树。对于非常深的树,这可能成为性能瓶颈。优化方法包括:

  • 在宿主元素上直接存储一个指向其对应 React Fiber 实例的引用,这样就不需要 WeakMap 查找。
  • EventDispatcher 内部缓存路径,或者在 Fiber 树上预计算路径信息(虽然这会增加内存开销)。

6. 抽象的力量

React 的事件插件系统是一个典范,它展示了如何通过深思熟虑的抽象层来解决复杂问题,并实现极高的灵活性和可扩展性。通过将原生事件的差异性、性能优化策略和跨平台 API 统一封装起来,它让开发者能够专注于业务逻辑,而无需关心底层宿主环境的细节。

为自定义渲染器实现一套类似的合成事件机制,不仅能让你深入理解 React 的内部工作原理,也为你的渲染器带来了与 React 生态系统无缝集成的能力。这使得你的自定义渲染器能够享受 React 声明式 UI、组件化、生命周期管理等带来的所有好处,同时又能在任何你想要的宿主环境中运行。这是一个复杂但回报丰厚的工程挑战,它赋予了我们构建真正跨平台应用的强大能力。

发表回复

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