React 事件系实现

一、前言

React 的合成事件系统是一套精密的机制,它并非简单地对原生事件进行封装,而是一个集成了事件委托、跨平台抽象优先级调度三大核心设计于一体的综合解决方案。本文将以源码导览的形式,深入这三大支柱。

1.1 事件委托

React 通过事件委托模式,极大地提升了应用的性能与可维护性。它并非为每个组件单独绑定事件,而是将几乎所有事件的监听器统一绑定在应用的根节点上。

  • 原理 :当一个元素的事件被触发时,该事件会沿着 DOM 树向上"冒泡"。React 在根节点捕获这个事件,并通过事件的 target 属性,反向追溯到触发事件的真实组件(Fiber 节点)。
  • 优势:显著减少了内存中的事件监听器数量,并简化了组件销毁时的垃圾回收,有效避免了内存泄漏。

注意 :并非所有事件都适合委托。对于那些原生不冒泡的事件(如 scroll, focus, blur),React 会选择在目标元素上单独进行监听。

1.2 跨平台抽象

为了抹平不同浏览器的实现差异,React 构建了一个强大的抽象层。这一层主要由两部分组成:

  • 合成事件对象 (SyntheticEvent) :这是开发者在事件回调中接触到的 e 对象。它封装了原生事件,并提供了一套标准、稳定的 API(如 e.stopPropagation(), e.preventDefault()),确保在所有浏览器中行为一致。
  • 事件插件系统 (Event Plugin System) :这是合成事件能够运作的底层架构。React 将不同事件的处理逻辑(如 clickchange)拆分到不同的插件中(SimpleEventPlugin, ChangeEventPlugin 等)。这种可插拔的设计,不仅隔离了复杂性,也使得 React 可以灵活地模拟或修复特定事件在某些浏览器上的行为。

1.3 优先级调度

为了确保 UI 的流畅响应,React 的事件系统与内部的调度器(Scheduler)和 Lane 优先级模型紧密相连。它将事件划分为不同的优先级:

  • 离散事件 (DiscreteEvent) :如 click, keydown。这类用户直接、期望立即得到反馈的交互,拥有最高优先级。
  • 连续事件 (ContinuousEvent) :如 mousemove, scroll。这类会持续触发的事件,优先级次之。
  • 默认事件 (DefaultEvent) :如 load, error 等。优先级更低。

当一个事件触发了状态更新(setState)时,该更新会继承事件的优先级。调度器会优先处理高优先级的更新,确保用户的点击等操作能得到最快响应,即使当时有其他低优先级的渲染任务正在进行。

二、事件委托与注册:从根节点到优先级分发器

React 的事件委托机制远比"在根节点上调用 addEventListener"要复杂。它是一套包含动态事件收集、优先级判断、分发器绑定和真实 DOM 监听的完整流程。

2.1 listenToAllSupportedEvents:事件的动态注册

当我们调用 ReactDOM.createRoot(root).render() 时,listenToAllSupportedEvents 会被调用,以确保所有 React 支持的事件都已被监听。

javascript 复制代码
// allNativeEvents 是通过遍历所有插件的 supportedEvents 聚合而成的 Set
// nonDelegatedEvents 包含 'scroll' 等,这些事件只在目标元素上监听,不参与委托
export function listenToAllSupportedEvents(rootContainerElement: EventTarget) {
  if (!rootContainerElement[listeningMarker]) {
    rootContainerElement[listeningMarker] = true;
    allNativeEvents.forEach((domEventName) => {
      if (domEventName !== "selectionchange") {
        // 对于非委托事件,只在捕获阶段监听
        if (!nonDelegatedEvents.has(domEventName)) {
          // 冒泡阶段的委托监听
          listenToNativeEvent(domEventName, false, rootContainerElement);
        }
        // 所有事件都在捕获阶段进行委托监听
        listenToNativeEvent(domEventName, true, rootContainerElement);
      }
    });
  }
}

2.2 listenToNativeEvent -> addTrappedEventListener:创建监听器

listenToNativeEvent 并不会直接调用 addEventListener,而是经过层层封装,最终由 addTrappedEventListener 完成。

javascript 复制代码
function listenToNativeEvent(
  domEventName: DOMEventName,
  isCapturePhase: boolean,
  target: EventTarget
) {
  let eventSystemFlags = 0;
  if (isCapturePhase) {
    eventSystemFlags |= IS_CAPTURE_PHASE; // 标记为捕获阶段
  }
  addTrappedEventListener(target, domEventName, eventSystemFlags);
}

function addTrappedEventListener(
  targetContainer: EventTarget,
  domEventName: DOMEventName,
  eventSystemFlags: EventSystemFlags
) {
  // 关键:创建带有优先级的监听器包裹函数
  const listener = createEventListenerWrapperWithPriority(
    targetContainer,
    domEventName,
    eventSystemFlags
  );

  // ... 此处还有 passive listener 的判断和 addEventListener 的调用逻辑 ...
  targetContainer.addEventListener(domEventName, listener, isCapturePhase);
}

2.3 createEventListenerWrapperWithPriority:连接委托与优先级

这是整个事件注册流程中最核心的函数之一。它根据事件的类型,返回一个特定优先级的分发函数 (dispatchDiscreteEventdispatchContinuousEvent 等),并将事件名、容器等信息通过 .bind 的方式预先传入。

javascript 复制代码
export function createEventListenerWrapperWithPriority(
  targetContainer: EventTarget,
  domEventName: DOMEventName,
  eventSystemFlags: EventSystemFlags
): Function {
  // 1. 获取事件的优先级
  const eventPriority = getEventPriority(domEventName);
  let listenerWrapper;

  // 2. 根据优先级,选择不同的分发器
  switch (eventPriority) {
    case DiscreteEventPriority: // e.g., click, keydown
      listenerWrapper = dispatchDiscreteEvent;
      break;
    case ContinuousEventPriority: // e.g., mousemove, scroll
      listenerWrapper = dispatchContinuousEvent;
      break;
    case DefaultEventPriority:
    default:
      listenerWrapper = dispatchEvent; // 通用分发器
      break;
  }

  // 3. 绑定参数,返回最终的监听器函数
  return listenerWrapper.bind(
    null,
    domEventName,
    eventSystemFlags,
    targetContainer
  );
}

补充:优先级的真正用途

这里根据 eventPriority 选择不同的 dispatch 函数,其意义远不止是选择一个函数名。真正的关键在于这些 dispatch 函数内部会通过 setCurrentUpdatePriority,将整个事件处理流程包裹在对应的优先级上下文中。

dispatchDiscreteEvent 的真实源码如下:

javascript 复制代码
function dispatchDiscreteEvent(
  domEventName: DOMEventName,
  eventSystemFlags: EventSystemFlags,
  container: EventTarget,
  nativeEvent: AnyNativeEvent
) {
  const prevTransition = ReactSharedInternals.T;
  ReactSharedInternals.T = null;
  const previousPriority = getCurrentUpdatePriority();
  try {
    // 关键:设置当前更新的优先级为离散事件优先级
    setCurrentUpdatePriority(DiscreteEventPriority);
    dispatchEvent(domEventName, eventSystemFlags, container, nativeEvent);
  } finally {
    // 保证在执行完毕后恢复之前的优先级
    setCurrentUpdatePriority(previousPriority);
    ReactSharedInternals.T = prevTransition;
  }
}

setCurrentUpdatePriority 会设置一个模块内的全局变量。这样做确保了后续在事件回调中(如 onClick)触发的任何状态更新(setState),都会通过 getCurrentUpdatePriority 读取到这个 DiscreteEventPriority。调度器据此便知这是一个高优先级任务,需要尽快执行(在 Concurrent 模式下通常会同步执行),从而保证了用户交互的即时响应。这正是 React 优先级调度机制与事件系统连接的枢纽。

至此,注册阶段完成。当一个原生 click 事件在根节点被触发时,实际执行的是被 bind 过的 dispatchDiscreteEvent 函数。

三、事件分发与调度:从分发器到插件系统

3.1 dispatchDiscreteEvent:高优先级事件分发

以离散事件为例,dispatchDiscreteEvent 会确保事件的同步执行,并在需要时处理并发渲染中的阻塞情况。

javascript 复制代码
function dispatchDiscreteEvent(
  domEventName,
  eventSystemFlags,
  container,
  nativeEvent
) {
  // ... 省略 ...

  // 最终会调用通用的 dispatchEvent 逻辑
  dispatchEvent(domEventName, eventSystemFlags, container, nativeEvent);
}

// 通用 dispatchEvent 的核心逻辑
function dispatchEvent(
  domEventName,
  eventSystemFlags,
  targetContainer,
  nativeEvent
) {
  // ...
  // 1. 检查在并发渲染中,是否有更高优先级的任务正在进行,导致当前事件被阻塞
  let blockedOn = findInstanceBlockingEvent(nativeEvent);
  if (blockedOn === null) {
    // 2. 如果没有阻塞,直接启动插件系统
    dispatchEventForPluginEventSystem(
      domEventName,
      eventSystemFlags,
      nativeEvent
      // ...
    );
    return;
  }

  // 3. 如果存在阻塞(通常发生在 Hydration 期间),则尝试同步完成阻塞的渲染任务
  // ... 此处包含 attemptSynchronousHydration 等复杂逻辑 ...
}

3.2 dispatchEventForPluginEventSystem:启动插件系统

这是从"事件分发"到"事件处理"的桥梁。它负责找到事件发生的真实目标 Fiber,并调用插件系统的中央分发器。

javascript 复制代码
function dispatchEventForPluginEventSystem(
  domEventName: DOMEventName,
  eventSystemFlags: EventSystemFlags,
  nativeEvent: AnyNativeEvent
  // ...
) {
  // 1. 从原生事件目标找到最近的 Fiber 实例
  const nativeEventTarget = getEventTarget(nativeEvent);
  const targetInst = getClosestInstanceFromNode(nativeEventTarget);

  // 2. 调用所有插件进行事件提取
  const dispatchQueue = [];
  extractEvents(
    dispatchQueue,
    domEventName,
    targetInst,
    nativeEvent
    // ...
  );

  // 3. 执行队列中的监听器
  processDispatchQueue(dispatchQueue, eventSystemFlags);
}

四、插件系统与跨平台抽象

4.1 extractEvents 中央分发器

DOMPluginEventSystem.js 中的 extractEvents 函数是插件系统的"派单中心"。它不自己处理逻辑,而是按顺序调用所有插件,让它们各自处理自己关心的事件。

javascript 复制代码
function extractEvents(
  dispatchQueue: DispatchQueue,
  domEventName: DOMEventName
  // ...
) {
  // 1. 首先调用最基础的 SimpleEventPlugin
  SimpleEventPlugin.extractEvents(
    dispatchQueue,
    domEventName
    // ...
  );

  // 2. 然后依次调用其他作为 Polyfill 的插件
  EnterLeaveEventPlugin.extractEvents(/* ... */);
  ChangeEventPlugin.extractEvents(/* ... */);
  SelectEventPlugin.extractEvents(/* ... */);
  BeforeInputEventPlugin.extractEvents(/* ... */);
  // ...
}

4.2 SimpleEventPlugin:标准事件处理流水线

作为最核心的插件,SimpleEventPluginextractEvents 负责处理大部分一对一的标准事件。

它的工作流程是:

  1. 识别事件 :通过 topLevelEventsToReactNames 映射表,将 click 转换为 onClick
  2. 选择构造函数 :通过一个 switch 语句,为 click 事件选择 SyntheticMouseEvent 作为合成事件的构造函数。
  3. 收集监听器 :调用 accumulateSinglePhaseListenersaccumulateTwoPhaseListeners,在 Fiber 树上从事件目标开始向上遍历,收集所有 props.onClickprops.onClickCapture 的监听器。
  4. 创建任务 :如果收集到监听器,则创建一个 SyntheticMouseEvent 实例,并和监听器数组一起打包成 {event, listeners} 对象,推入 dispatchQueue

4.3 ChangeEventPlugin:复杂事件的模拟

ChangeEventPlugin 则展示了插件系统如何作为 Polyfill 抹平浏览器差异。它会监听 input, keydown, click 等多个原生事件,通过内部状态追踪,最终模拟出在所有表单元素上行为都统一的 onChange 事件,并将其任务推入 dispatchQueue

五、队列执行与回调触发

5.1 processDispatchQueueexecuteDispatch

在所有插件完成工作后,processDispatchQueue 会遍历 dispatchQueue 队列,并根据当前是捕获还是冒泡阶段,以正确的顺序执行所有收集到的监听器。

javascript 复制代码
export function processDispatchQueue(
  dispatchQueue: DispatchQueue,
  eventSystemFlags: EventSystemFlags
): void {
  const isCapturePhase = (eventSystemFlags & CAPTURE_PHASE) !== 0;
  for (let i = 0; i < dispatchQueue.length; i++) {
    const { event, listeners } = dispatchQueue[i];
    if (isCapturePhase) {
      // 捕获阶段:正序执行
      for (let j = 0; j < listeners.length; j++) {
        executeDispatch(event, listeners[j]);
        if (event.isPropagationStopped()) break;
      }
    } else {
      // 冒泡阶段:逆序执行
      for (let j = listeners.length; j-- > 0; ) {
        executeDispatch(event, listeners[j]);
        if (event.isPropagationStopped()) break;
      }
    }
  }
}

function executeDispatch(event: ReactSyntheticEvent, listener: Function): void {
  // 在执行前检查事件是否已被停止传播
  if (event.isPropagationStopped()) {
    return;
  }
  // 真正执行用户定义的回调函数
  listener(event);
}

这个过程清晰地展示了 React 事件系统是如何解耦"收集"和"执行"这两个步骤的,为批处理和并发渲染中的复杂调度提供了可能。

六、整体流程图

七、总结

回顾 React 事件系统,三个技术贯穿始终:一是委托 + 合成事件,既减少监听数量又抹平浏览器差异,解决原生事件的性能与兼容痛点;二是插件化架构,将不同类型事件(如鼠标、表单、进入离开)的处理逻辑解耦,方便扩展与维护;三是优先级协同,通过离散 / 连续事件的优先级划分,让用户关键交互(点击、输入)优先响应,平衡流畅度与主线程效率。

相关推荐
一颗烂土豆1 小时前
🚀从 autofit 到 vfit:Vue 开发者该选哪个大屏适配工具?
前端·vue.js
z_mazin1 小时前
逆向Sora 的 Web 接口包装成了标准的 OpenAI API 格式-系统架构
linux·运维·前端·爬虫·系统架构
CoolerWu1 小时前
Trae Solo 实战指南:从"会用"到"用好"的协作方法论
前端·javascript
听风说图1 小时前
Figma画布协议揭秘:组件实例的SymbolOverrides覆盖机制
前端·canvas
小杨前端1 小时前
前端如何自己实现一个webpack的热更新?
前端
@大迁世界1 小时前
02.CSS变量 (Variables)
前端·css
鹏多多1 小时前
轻量+响应式!React瀑布流插件react-masonry-css的详细教程和案例
前端·javascript·react.js
用户345848285051 小时前
java中的tomicInteger/AtomicLong介绍
前端·后端
一颗宁檬不酸1 小时前
Vue.js 初学者基础知识点总结 第一弹
前端·javascript·vue.js