Skip to content

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)。

派生事件:通过“继承”扩展

更具体的事件类型,如 SyntheticUIEventSyntheticMouseEvent,是通过扩展基础 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);

设计决策与解析:

  1. 组合与继承:通过 JavaScript 的对象扩展语法(...),MouseEventInterface 继承了 UIEventInterface 的所有属性,而 UIEventInterface 又继承了 EventInterface。这种方式清晰地表达了不同事件类型之间的层级关系。
  2. 属性规范化MouseEventInterface 中的 relatedTargetmovementX 都是函数。当创建 SyntheticMouseEvent 实例时,构造函数会执行这些函数,并传入原生事件对象。这些函数内部包含了处理浏览器兼容性问题的逻辑,确保最终的 syntheticEvent.relatedTarget 在所有浏览器上都有一个可靠的值。
  3. preventDefaultstopPropagation:这两个核心方法被添加到了 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 事件的复杂性和兼容性问题。

Last updated: