各位来宾,大家好!
今天,我们将深入探讨 React 核心机制之一——’Event Plugin System’,并在此基础上,共同探索如何为我们自己的自定义渲染器(Custom Renderer)构建一套同样强大且跨平台的合成事件机制。这是一个充满挑战但又极其有益的话题,它将揭示 React 能够如此灵活地在不同宿主环境(如 DOM、Native、Canvas 等)中运行的奥秘。
1. React 合成事件的必要性
在我们直接 diving into React 的事件系统之前,让我们先思考一个基本问题:为什么 React 不直接使用浏览器原生的 DOM 事件?
原因有以下几点:
- 跨浏览器兼容性: 不同的浏览器在原生事件的实现上存在差异,例如事件对象的属性、事件的冒泡/捕获行为、事件的默认行为处理等。直接使用原生事件会导致开发者需要处理大量的兼容性代码。
- 性能优化: 在大型应用中,为每个 DOM 元素都附加事件监听器会消耗大量内存和 CPU 资源。React 通过事件委托(Event Delegation)机制,在根节点上统一处理事件,显著提升了性能。
- 一致的 API: React 希望提供一套统一、简洁的事件处理 API,无论底层宿主环境是 DOM 还是其他(如 React Native 的 View),开发者都能以相同的方式编写事件处理逻辑。
- 事件池(Event Pooling): 虽然在 React 17 之后已被废弃,但在早期版本中,事件池用于复用事件对象,减少垃圾回收的压力,进一步优化性能。
- 生命周期管理: React 组件有其自身的生命周期。当组件挂载、更新或卸载时,其相关的事件监听器也需要被正确地添加或移除。合成事件系统与 React 的协调(Reconciliation)过程紧密结合,确保了事件处理与组件生命周期的一致性。
正是基于这些需求,React 引入了“合成事件”(Synthetic Events)的概念,它作为一层抽象,封装了原生事件的复杂性,提供了一个统一、高性能且易于使用的事件处理层。
2. React 事件插件系统的核心概念
React 的事件插件系统是一个高度模块化和可扩展的设计。它由几个关键概念组成:
2.1. 事件委托 (Event Delegation)
这是 React 事件系统最基础也是最重要的优化策略。React 不会将事件监听器直接附加到你 JSX 中定义的每个 DOM 元素上。相反,它会在应用的根节点(通常是 document 或 ReactDOM.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(): 阻止事件对象被放入池中(如果事件池启用),允许异步访问。- 其他特定事件类型(如
SyntheticMouseEvent、SyntheticKeyboardEvent)还会包含额外的属性,例如clientX,clientY,keyCode,altKey等。
2.3. 事件插件 (Event Plugins)
事件插件是 React 事件系统的核心扩展点。它们是负责处理特定类型事件的模块。每个插件都注册了一组它感兴趣的 React 事件名称(例如 onClick、onMouseDown)以及它们对应的原生事件依赖(例如 click、mousedown)。
当原生事件发生时,事件系统会遍历所有注册的插件,让它们有机会“提取”(extract)出相关的合成事件对象,并将其放入一个待分发的队列中。
React 内部有多种事件插件,例如:
SimpleEventPlugin: 处理大多数常见的 DOM 事件,如click,mousedown,input,change等。EnterLeaveEventPlugin: 处理onMouseEnter,onMouseLeave等特殊事件,它们不依赖于 DOM 原生的mouseover和mouseout的冒泡行为。ChangeEventPlugin: 专门处理表单元素的onChange事件,使其在不同浏览器和不同输入类型(input,select,textarea)下行为一致。SelectEventPlugin: 处理onSelect事件。
2.4. 事件调度队列 (Dispatch Queue)
当一个原生事件被捕获并经过插件处理后,系统会构建一个事件调度队列。这个队列包含了所有需要被触发的 React 事件处理器及其对应的合成事件对象。队列会按照捕获阶段(从根到目标)和冒泡阶段(从目标到根)的顺序排列。
2.5. 事件注册与映射
React 内部维护了一个注册表,将 JSX 中的事件属性名(如 onClick)映射到其对应的原生事件类型(如 click)以及处理这些事件的插件。这个注册表是动态的,允许插件在初始化时注册其感兴趣的事件。
3. React 事件系统工作流 (简化版)
为了更好地理解,我们来概括一下 React 事件从原生到合成的简化流程:
-
应用启动/渲染:
ReactDOM.render()或createRoot().render()会在指定的根 DOM 节点(或document)上注册少数几个最顶层的原生事件监听器(例如click,keydown,input等,这些都是由各个EventPlugin声明的依赖)。- 同时,React 会初始化并加载所有
EventPlugin。
-
用户交互 (原生事件发生):
- 用户在某个子 DOM 元素上执行操作,例如点击一个按钮。
- 原生
click事件从目标元素开始冒泡,直到被根节点上的监听器捕获。
-
事件分发器捕获:
- 根节点上的原生事件监听器被触发。它接收到原生
MouseEvent对象。
- 根节点上的原生事件监听器被触发。它接收到原生
-
识别目标 React 实例:
- React 会通过内部机制(例如,从原生
event.targetDOM 元素向上查找,找到与之对应的 React Fiber 实例)来确定事件的“目标 React 实例”(targetInst)。
- React 会通过内部机制(例如,从原生
-
插件提取事件:
- 事件系统遍历所有已注册的
EventPlugin。 - 每个插件的
extractEvents方法被调用,传入原生事件、目标 React 实例等信息。 - 如果插件识别出当前原生事件与它负责的某个 React 事件类型相关(例如
SimpleEventPlugin识别出click事件需要生成onClick的合成事件),它就会创建一个或多个SyntheticEvent实例(例如SyntheticMouseEvent)。 - 这些合成事件连同其对应的处理器(通过遍历 React 实例的属性找到)被添加到事件调度队列中。
- 事件系统遍历所有已注册的
-
构建调度队列:
- 事件系统根据
targetInst和currentTargetInst的关系,向上遍历 React Fiber 树,收集所有沿途的onClick(捕获和冒泡)处理器,并将其与对应的SyntheticEvent对象打包成{ listener, event }对,添加到调度队列中。
- 事件系统根据
-
事件调度:
- 事件系统按照捕获阶段(从根到目标)和冒泡阶段(从目标到根)的顺序,依次执行调度队列中的所有处理器。
- 在执行过程中,
SyntheticEvent对象的currentTarget属性会动态更新,指向当前正在处理事件的 React 元素。 - 如果某个处理器调用了
event.stopPropagation(),则会阻止后续的冒泡/捕获阶段的处理器被调用。 - 如果调用了
event.preventDefault(),则会阻止原生事件的默认行为。
-
事件对象清理 (旧版本):
- 在 React 17 之前,
SyntheticEvent对象会被放回事件池中以供复用。现在,事件对象会在事件处理完成后被销毁。
- 在 React 17 之前,
这个流程确保了 React 事件处理的统一性、高效性和可扩展性。
4. 为自定义渲染器实现跨平台合成事件机制
现在,我们面临真正的挑战:如何为我们自己的自定义渲染器(例如,一个基于 Canvas 或 WebGL 的渲染器,或者一个命令行界面渲染器)实现一套类似的跨平台合成事件机制?
假设我们正在构建一个名为 MyCustomRenderer 的渲染器,它不再操作 DOM 元素,而是操作我们自定义的“宿主元素”(MyHostElement),例如 Canvas 上的一个形状、一个 WebGL 对象或 CLI 上的一个文本块。这些 MyHostElement 不具备原生的 DOM 事件。
4.1. 核心挑战
- 没有原生 DOM 事件: 我们需要定义自己的“原生事件”或将外部输入(如鼠标坐标、键盘按键)转换为我们渲染器能够理解的“宿主原生事件”。
- 自定义宿主元素树: 我们需要维护一个类似于 DOM 树的“宿主元素树”,以便进行事件冒泡和捕获。
- 事件源: 如何从宿主环境(例如 Canvas 的
canvas.addEventListener)捕获原始输入,并将其桥接到我们的事件系统? - React Fiber 实例映射: 如何从一个
MyHostElement找到与之对应的 React Fiber 实例,以便查找其上的事件处理器?
4.2. 我们的自定义事件系统架构
为了解决这些挑战,我们将设计一个与 React 现有系统类似的架构,包含以下核心组件:
MyNativeEvent接口: 定义我们自定义渲染器中的“原生事件”结构。SyntheticEvent基类及派生类: 我们的跨平台合成事件对象。EventPlugin接口: 定义事件插件的契约。EventRegistry: 注册 React 事件名称与原生事件类型的映射。EventDispatcher: 负责从宿主环境接收MyNativeEvent,通过插件提取合成事件,并调度给 React 组件。- 宿主元素与 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 中做以下几件事:
-
存储事件监听器: 当 React 创建或更新一个宿主元素时,它会提供
props。我们需要将onClick、onMouseDown等事件处理器从props中提取出来,并存储在宿主元素或 React Fiber 实例的某个地方,以便EventDispatcher能够检索到它们。- 在
createInstance(type, props, ...)和commitUpdate(instance, updatePayload, ...)方法中处理。 - 通常,我们会将这些监听器存储在 React Fiber 实例的
memoizedProps或stateNode(即MyHostElement)的某个内部属性上。
- 在
-
提供
getInstanceFromNode和getListenersFromInstance:EventDispatcher依赖这两个方法来将MyHostElement映射回 React Fiber 实例,并从 Fiber 实例中获取事件监听器。getInstanceFromNode(node: MyHostElement): 这需要你在createInstance时建立一个从MyHostElement到 React Fiber 实例的映射(例如,一个WeakMap)。getListenersFromInstance(inst: any, reactPropName: string): 这个方法会根据reactPropName(如onClick)从inst.memoizedProps中获取对应的函数。
-
在根节点附加
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) 紧密集成。当事件监听器抛出错误时,错误会被捕获并传播到最近的错误边界。在我们的自定义系统中,EventDispatcher 的 handleEvent 方法中的 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 事件,你可能还需要为你的渲染器定义完全自定义的事件,例如 onDragStart、onZoom 等。你可以通过创建新的 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、组件化、生命周期管理等带来的所有好处,同时又能在任何你想要的宿主环境中运行。这是一个复杂但回报丰厚的工程挑战,它赋予了我们构建真正跨平台应用的强大能力。