Appearance
6.4 合成事件(SyntheticEvent)的实现
在 React 的世界里,我们从事件处理器中接收到的 event 对象并不是原生的 DOM 事件,而是一个名为 SyntheticEvent 的实例。它是 React 事件系统的核心组成部分,旨在抹平不同浏览器原生事件的差异,提供一个统一、稳定且符合 W3C 规范的事件接口。
SyntheticEvent 的实现位于 packages/react-dom-bindings/src/events/SyntheticEvent.js 文件中,其设计思想体现了“包装”和“继承”的巧妙结合。
工厂函数:createSyntheticEvent
React 并没有直接定义一个 SyntheticEvent 类,而是使用了一个名为 createSyntheticEvent 的工厂函数来动态创建事件构造器。这样做是为了性能优化:如果所有合成事件都使用同一个构造函数,那么这个构造函数会因为处理不同形状的事件对象而变得“巨型”(megamorphic),导致 JavaScript 引擎难以优化。通过工厂模式为不同类型的事件(如 MouseEvent, KeyboardEvent)创建不同的构造器,可以避免这个问题。
javascript
// In packages/react-dom-bindings/src/events/SyntheticEvent.js
// 这是一个工厂函数,用于创建不同类型的合成事件构造器
function createSyntheticEvent(Interface: EventInterfaceType) {
// SyntheticBaseEvent 是所有合成事件的基类
function SyntheticBaseEvent(
reactName: string | null,
reactEventType: string,
targetInst: Fiber | null,
nativeEvent: {[propName: string]: mixed, ...},
nativeEventTarget: null | EventTarget,
) {
this._reactName = reactName;
this._targetInst = targetInst;
this.type = reactEventType;
this.nativeEvent = nativeEvent; // 保留对原生事件的引用
this.target = nativeEventTarget;
this.currentTarget = null;
// 从 Interface 复制属性到 this
for (const propName in Interface) {
if (!Interface.hasOwnProperty(propName)) {
continue;
}
const normalize = Interface[propName];
if (normalize) {
// 如果需要规范化,则调用规范化函数
this[propName] = normalize(nativeEvent);
} else {
// 否则直接从原生事件复制
this[propName] = nativeEvent[propName];
}
}
// ... (处理 defaultPrevented 和 propagationStopped)
return this;
}
assign(SyntheticBaseEvent.prototype, {
preventDefault: function() { /* ... */ },
stopPropagation: function() { /* ... */ },
persist: function() { /* no-op in modern React */ },
isPersistent: functionThatReturnsTrue,
});
return SyntheticBaseEvent;
}事件接口(Interface):定义事件的“形状”
createSyntheticEvent 函数接收一个 Interface 对象作为参数。这个 Interface 定义了特定类型合成事件应该具有的属性,以及如何从原生事件中获取这些属性的值。
这正是 React 抹平浏览器差异的核心所在。
javascript
// In packages/react-dom-bindings/src/events/SyntheticEvent.js
/**
* @interface Event
* @see http://www.w3.org/TR/DOM-Level-3-Events/
*/
const EventInterface: EventInterfaceType = {
eventPhase: 0,
bubbles: 0,
cancelable: 0,
timeStamp: function (event) {
return event.timeStamp || Date.now(); // 兼容性处理
},
defaultPrevented: 0,
isTrusted: 0,
};
// 创建最基础的 SyntheticEvent 构造器
export const SyntheticEvent: $FlowFixMe = createSyntheticEvent(EventInterface);EventInterface 定义了所有事件共有的基本属性。注意 timeStamp 属性:它是一个函数,用于处理某些浏览器可能没有 event.timeStamp 的情况。这就是所谓的“规范化”(normalization)。
派生事件:通过“继承”扩展
更具体的事件类型,如 SyntheticUIEvent 或 SyntheticMouseEvent,是通过扩展基础 Interface 来创建的。这类似于面向对象编程中的类继承。
javascript
// In packages/react-dom-bindings/src/events/SyntheticEvent.js
// UIEvent 接口继承自 EventInterface
const UIEventInterface: EventInterfaceType = {
...EventInterface,
view: 0,
detail: 0,
};
export const SyntheticUIEvent: $FlowFixMe =
createSyntheticEvent(UIEventInterface);
// MouseEvent 接口继承自 UIEventInterface
const MouseEventInterface: EventInterfaceType = {
...UIEventInterface,
screenX: 0,
screenY: 0,
clientX: 0,
clientY: 0,
pageX: 0, // 规范化函数,处理 pageX/pageY 的兼容性
pageY: 0,
ctrlKey: 0,
shiftKey: 0,
altKey: 0,
metaKey: 0,
button: 0,
buttons: 0,
relatedTarget: function (event) {
// 规范化 relatedTarget
if (event.relatedTarget === undefined)
return event.fromElement === event.srcElement
? event.toElement
: event.fromElement;
return event.relatedTarget;
},
movementX: function (event) {
// 规范化 movementX
if (event.movementX !== undefined) {
return event.movementX;
}
// Polyfill for movementX
// ...
return lastMovementX;
},
movementY: function (event) { /* ... */ },
};
export const SyntheticMouseEvent: $FlowFixMe =
createSyntheticEvent(MouseEventInterface);设计决策与解析:
- 组合与继承:通过 JavaScript 的对象扩展语法(
...),MouseEventInterface继承了UIEventInterface的所有属性,而UIEventInterface又继承了EventInterface。这种方式清晰地表达了不同事件类型之间的层级关系。 - 属性规范化:
MouseEventInterface中的relatedTarget和movementX都是函数。当创建SyntheticMouseEvent实例时,构造函数会执行这些函数,并传入原生事件对象。这些函数内部包含了处理浏览器兼容性问题的逻辑,确保最终的syntheticEvent.relatedTarget在所有浏览器上都有一个可靠的值。 preventDefault和stopPropagation:这两个核心方法被添加到了SyntheticBaseEvent的原型上。它们的实现逻辑很简单:调用原生事件对应的preventDefault()或stopPropagation()方法,同时设置一个内部标志位(如this.isPropagationStopped = functionThatReturnsTrue),以便 React 的事件系统能够查询事件的状态。
告别事件池化(Event Pooling)
值得注意的是,在 React 17 及更早版本中,SyntheticEvent 对象是“池化”的,以减少内存分配。这意味着在事件回调执行后,事件对象会被回收并重置。然而,从 React 18 开始,事件池化被移除了。这是因为现代 JavaScript 引擎在垃圾回收方面已经足够高效,池化带来的性能优势不再明显,反而增加了代码的复杂性和开发者的心智负担(例如,不能在异步回调中直接访问事件属性)。
在 SyntheticEvent.js 中,我们仍然可以看到 persist() 方法的存留,但它已经是一个空操作(no-op),这正是为了保持向后兼容性。
总结
SyntheticEvent 是 React 事件系统优雅设计的典范。通过工厂模式、接口定义和原型继承,React 以一种高效、可扩展且类型安全的方式,构建了一个强大的跨浏览器事件系统。它将浏览器的不一致性隐藏在内部,为开发者提供了一个干净、可靠的 API,让我们能够专注于业务逻辑,而无需担心底层 DOM 事件的复杂性和兼容性问题。