Skip to content

事件插件机制

React 的事件插件机制是一种模块化的设计,旨在处理不同类型的事件,并提高性能。它允许 React 在事件触发时根据不同的事件类型使用不同的插件,从而避免了在每个事件类型上重复相同的逻辑. 事件插件机制的核心思想如下:

  1. 事件合成:

React 内部的事件系统使用合成事件(SyntheticEvent) 来封装浏览器原生事件。这些合成事件在事件触发时被创建,并且具有跨浏览器的兼容性.

  1. 插件注册:

在 React 渲染过程中,会注册不同的事件插件,每个插件负责处理特定的事件类型.

  1. 事件分发:

当用户与页面交互时,事件触发。React 会根据事件类型,调用对应的插件来生成合成事件对象.

  1. 事件处理:

每个插件会将合成事件对象分发给相应的事件处理函数,事件处理函数执行后,插件会回收合成事件对象,以避免内存泄漏.

常见事件插件示例:

  • SimpleEventPlugin:

处理常用的事件,例如 blur, focus, click, submit, touchMove, mouseMove, scroll 和 drag.

  • EnterLeaveEventPlugin:

处理 mouseEnter 和 mouseLeave 事件,以及 pointerEnter 和 pointerLeave 事件,因为它们不支持冒泡.

  • TapEventPlugin:

解决移动端 iOS 的 300ms 点击延迟问题,通过添加一个 onTouchTap 事件来避免延迟.

  • ChangeEventPlugin:

处理表单的 onChange 事件,通过捕获多个事件,如 blur, change, focus, input 等,来模拟 onChange 事件.

插件

每一个插件都需要向外暴露两个方法

  • registerEvents

用于插件中合成事件的注册,这样在插件初始化的时候就在全局注册了当前 合成事件 依赖的原生事件数组,从而在原生事件触发的时候判断是否存在当前合成插件去调用其回调

onClick => click ; onChange => ["change", "click", "focusin", "focusout", "input", "keydown", "keyup", "selectionchange"]

  • extractEvents

用于插件中合成事件的回调,当注册的合成事件对应的原生事件触发了,那么就回调 extractEvents 函数, 并将事件对象作为参数传入,在 extractEvents 函数中,会根据事件对象的类型,调用不同的事件处理函数,从而实现事件的处理.

那么 React 是如何管理整个插件的呐,这个在 React 初始化的时候就执行了 5 种类型的事件插件的注册

  • SimpleEventPlugin.registerEvents();
  • EnterLeaveEventPlugin.registerEvents();
  • ChangeEventPlugin.registerEvents();
  • SelectEventPlugin.registerEvents();
  • BeforeInputEventPlugin.registerEvents();

每一个插件中都通过 registerDirectEvent('onSelect', ['focusout','contextmenu','dragend','focusin','keydown','keyup','mousedown','mouseup','selectionchange']); 来注册当前合成事件依赖的原生事件数组,这样通过事件委托的机制就可以在触发对应原生事件的时候,根据 target 找到 dom 节点 和 其对应的 FiberNode. 然后根据 FiberNode 上是否定义对应的 合成事件,找到当前合成事件关联的 原生事件是否包含当前 eventName, 这样就形成了 合成事件 和原生事件的绑定

具体如下

js
/**
 * 注册同步优先级的事件
 * @param {*} registrationName
 * @param {*} dependencies
 */
export function registerDirectEvent(
  registrationName: string, // 合成事件名称
  dependencies: Array<DOMEventName> // 原生事件名称
) {
  // 缓存事件的依赖关系, 在开发环境下,用于警告二次绑定
  registrationNameDependencies[registrationName] = dependencies;

  // 缓存所有需要监听的原生事件 click
  for (let i = 0; i < dependencies.length; i++) {
    allNativeEvents.add(dependencies[i]);
  }
}

核心是维护了两个全局对象: registrationNameDependenciesallNativeEvents,

  • registrationNameDependencies

对象格式,缓存了合成事件和其关联的原生事件数组, 例如

js
{
  onSelect: ['focusout','contextmenu','dragend','focusin','keydown','keyup','mousedown','mouseup','selectionchange'],
}
  • allNativeEvents

这是一个 Set 类型的全局对象,缓存了所有需要监听的原生事件,记住这个属性,后面需要使用

例外还有一个函数 registerTwoPhaseEvent

registerTwoPhaseEvent

js
export function registerTwoPhaseEvent(
  registrationName: string,
  dependencies: Array<DOMEventName>
): void {
  // 注册 合成事件与原生事件的关系
  // 比如 onClick => click
  registerDirectEvent(registrationName, dependencies);
  // 注册合成事件中原生事件 与 原生事件的关系
  // 比如 onClickCapture => click
  registerDirectEvent(registrationName + "Capture", dependencies);
}

通过两次 registerDirectEvent 缓存了 合成事件 => 原生事件 , 合成事件(捕获阶段) => 原生事件 的关系,即如一个 click 事件,其就有两个合成事件名称 和 一个原生事件

js
registrationNameDependencies = {
  onClick: ["click"],
  onClickCapture: ["click"],
};

allNativeEvents = ["click"];