Skip to content

6.3 事件的分发和执行:监听器的收集与执行

在上一节中,我们看到 extractEvents 函数成功地将一个原生 DOM 事件转换成了一个或多个 React 合成事件,并将它们连同对应的监听器(listeners)一起推入了 dispatchQueue 队列。现在,是时候处理这个队列,真正地执行我们在组件中定义的事件处理函数了。

这个过程主要由 processDispatchQueueprocessDispatchQueueItemsInOrder 这两个函数驱动,它们是 React 事件分发机制的核心。

监听器的收集:accumulateSinglePhaseListeners

在深入分发过程之前,我们必须先理解监听器是如何被收集的。这个任务由 accumulateSinglePhaseListeners 函数完成(该函数在 SimpleEventPlugin.js 内部被调用)。

它的核心职责是:从触发事件的 Fiber 节点开始,沿着 Fiber 树向上遍历直到根节点,收集所有路径上为当前事件(例如 onClick)定义的捕获和冒泡阶段的监听器。

javascript
// In packages/react-dom-bindings/src/events/DOMPluginEventSystem.js

function accumulateSinglePhaseListeners(
  targetFiber: Fiber | null,
  reactName: string,
  nativeEventType: string,
  inCapturePhase: boolean,
  accumulateTargetOnly: boolean,
  eventSystemFlags: EventSystemFlags,
): Array<DispatchListener> {
  const bubbledName = reactName;
  const capturedName = reactName + 'Capture';
  const listeners: Array<DispatchListener> = [];
  let instance = targetFiber;

  // 遍历从目标 Fiber 到根节点的路径
  while (instance !== null) {
    const {stateNode, tag} = instance;
    // 只在 HostComponent(DOM 元素)上查找监听器
    if (tag === HostComponent && stateNode !== null) {
      const currentTarget = stateNode;
      if (inCapturePhase) {
        // 捕获阶段,查找 onClickCapture
        const listener = getListener(instance, capturedName);
        if (listener) {
          listeners.push(createDispatchListener(instance, listener, currentTarget));
        }
      } else {
        // 冒泡阶段,查找 onClick
        const listener = getListener(instance, bubbledName);
        if (listener) {
          listeners.push(createDispatchListener(instance, listener, currentTarget));
        }
      }
    }
    instance = instance.return;
  }
  return listeners;
}

设计决策与解析:

  1. 沿着 Fiber 树遍历:React 的组件层级关系体现在 Fiber 树上。通过 instance.return 向上遍历,可以完美模拟 DOM 的事件冒泡/捕获路径。
  2. 区分捕获与冒泡:通过 inCapturePhase 标志,函数可以准确地收集 onClickCapture(捕获)或 onClick(冒泡)的监听器。React 事件系统完整地在内部模拟了 W3C 的事件流模型。
  3. 性能优化getListener 函数直接从 Fiber 节点的 memoizedProps 中获取事件处理器。这意味着事件监听器的查找速度非常快,因为它只是一个对象属性的访问,而无需查询真实的 DOM 元素。
  4. DispatchListener 对象:收集到的不是裸露的函数,而是一个 DispatchListener 对象,它封装了 listener(处理器函数)、instance(Fiber 节点)和 currentTarget(当前的 DOM 元素)。这为后续的事件执行提供了完整的上下文。

事件队列的处理:processDispatchQueue

extractEvents 完成它的工作后,dispatchEventsForPlugins 函数会立即调用 processDispatchQueue 来处理 dispatchQueue

javascript
// In packages/react-dom-bindings/src/events/DOMPluginEventSystem.js

export function processDispatchQueue(
  dispatchQueue: DispatchQueue,
  eventSystemFlags: EventSystemFlags,
): void {
  // 判断当前是否处于捕获阶段
  const inCapturePhase = (eventSystemFlags & IS_CAPTURE_PHASE) !== 0;
  // 遍历队列中的每一个事件项
  for (let i = 0; i < dispatchQueue.length; i++) {
    const {event, listeners} = dispatchQueue[i];
    // 按顺序执行该事件的所有监听器
    processDispatchQueueItemsInOrder(event, listeners, inCapturePhase);
  }
}

这个函数本身很简单,它只是一个循环,但它揭示了一个重要的设计点:

  • 批量处理dispatchQueue 的存在使得 React 可以将由一个原生事件触发的多个 React 事件(例如,focusin 同时触发 onFocusonFocusIn)收集起来,然后一次性按顺序处理。

按序执行监听器:processDispatchQueueItemsInOrder

这是事件执行的最后一站。它接收一个事件对象和一组监听器,并负责以正确的顺序调用它们。

javascript
// In packages/react-dom-bindings/src/events/DOMPluginEventSystem.js

function processDispatchQueueItemsInOrder(
  event: ReactSyntheticEvent,
  dispatchListeners: Array<DispatchListener>,
  inCapturePhase: boolean,
): void {
  let previousInstance;
  if (inCapturePhase) {
    // 捕获阶段:从后往前执行监听器(从父到子)
    for (let i = dispatchListeners.length - 1; i >= 0; i--) {
      const {instance, currentTarget, listener} = dispatchListeners[i];
      if (instance !== previousInstance && event.isPropagationStopped()) {
        return; // 如果事件停止传播,则中断执行
      }
      executeDispatch(event, listener, currentTarget);
      previousInstance = instance;
    }
  } else {
    // 冒泡阶段:从前往后执行监听器(从子到父)
    for (let i = 0; i < dispatchListeners.length; i++) {
      const {instance, currentTarget, listener} = dispatchListeners[i];
      if (instance !== previousInstance && event.isPropagationStopped()) {
        return; // 如果事件停止传播,则中断执行
      }
      executeDispatch(event, listener, currentTarget);
      previousInstance = instance;
    }
  }
}

设计决策与解析:

  1. 模拟 DOM 事件流

    • 对于捕获阶段 (inCapturePhasetrue),循环从后往前遍历 dispatchListeners 数组。因为 accumulateSinglePhaseListeners 是从目标向根节点收集的,所以数组的末尾是离根节点最近的组件,最前面是离目标最近的组件。从后往前遍历正好实现了从父到子的捕获流。
    • 对于冒泡阶段 (inCapturePhasefalse),循环从前往后遍历,实现了从子到父的冒泡流。
  2. 支持 stopPropagation:在执行每个监听器之前,都会检查 event.isPropagationStopped()。如果在任何一个监听器内部调用了 event.stopPropagation(),这个函数会返回 true,从而中断后续监听器的执行。这是对标准 DOM API 的忠实模拟。

  3. executeDispatch:这是一个简单的包装函数,它负责设置 event.currentTarget(这对于事件处理非常重要),然后在一个 try...catch 块中安全地执行监听器,并捕获任何可能发生的错误,防止单个事件处理器的错误导致整个应用崩溃。

javascript
// In packages/react-dom-bindings/src/events/DOMPluginEventSystem.js

function executeDispatch(
  event: ReactSyntheticEvent,
  listener: Function,
  currentTarget: EventTarget,
): void {
  // 在执行监听器前,正确设置 currentTarget
  event.currentTarget = currentTarget;
  try {
    listener(event);
  } catch (error) {
    // 捕获错误并报告,而不是让整个应用崩溃
    reportGlobalError(error);
  }
  // 执行完毕后,重置 currentTarget
  event.currentTarget = null;
}

总结

React 的事件分发和执行机制是一个精心设计的系统,它通过在内部模拟 DOM 的事件流,实现了高性能、跨浏览器一致且功能强大的事件处理能力。

整个流程可以总结为:

  1. 收集accumulateSinglePhaseListeners 沿 Fiber 树向上遍历,收集捕获和冒泡的监听器。
  2. 入队extractEvents 将合成事件和收集到的监听器打包,推入 dispatchQueue
  3. 处理processDispatchQueue 遍历队列,对每个事件-监听器组合进行处理。
  4. 执行processDispatchQueueItemsInOrder 根据捕获或冒泡阶段,按正确顺序遍历监听器数组,并通过 executeDispatch 安全地执行每一个事件处理器。

这个流程清晰地将“事件提取”、“监听器收集”和“事件执行”三个阶段解耦,每个部分都只做一件事,并把它做好。正是这种清晰的架构,使得 React 的事件系统既稳定又易于扩展。

Last updated: