React 17 事件机制源码深度解析:从委托绑定到执行流程

作者:星野时间:2026-10-24 09:25:13浏览:539

本文深入解析 React 17.0.1 版本的事件机制源码。重点阐述事件在 fiberRoot 创建时的委托绑定策略,区分离散、用户阻塞及连续三种事件优先级。详细梳理从原生事件冒泡到合成事件收集、再到 dispatchQueue 执行的全链路流程,并解释原生 stopPropagation 对合成事件的影响原理。

在 React v17 版本中,事件机制发生了显著变化,与 v16 相比存在较大差异。

本文基于 React 17.0.1 版本进行源码分析,通过 ReactDOM.render 创建应用,暂不涉及优先级相关逻辑。

原理简述

React 将事件划分为委托事件(DelegatedEvent)和非委托事件(NonDelegatedEvent)。对于委托事件,在 fiberRoot 创建阶段,React 会在根节点的 DOM 元素上绑定几乎所有事件的处理函数;而非委托事件则直接将处理函数绑定在对应的 DOM 元素上。

此外,React 将事件分为三种类型:discreteEvent、userBlockingEvent 和 continuousEvent。这三类事件拥有不同的优先级,在绑定处理函数时会调用不同的回调函数。

React 事件机制建立在原生事件基础之上,模拟了一套冒泡和捕获机制。当某个 DOM 元素触发事件后,事件会冒泡至 React 绑定在 root 节点的处理函数。React 通过 target 获取触发事件的 DOM 对象及其对应的 Fiber 节点,随后从该 Fiber 节点向上层父级遍历,收集一条事件队列。接着遍历该队列以触发其中每个 Fiber 对象对应的事件处理函数:正向遍历模拟冒泡过程,反向遍历模拟捕获过程。因此,合成事件的触发时机位于原生事件之后。

Fiber 对象对应的事件处理函数实际上存储在 props 中,收集过程仅是从 props 中提取,并未直接绑定到任何 DOM 元素上。

源码浅析

以下源码仅为基础逻辑的简化分析,旨在理清事件机制的触发流程,省略了大量与流程无关或过于复杂的代码。

委托事件绑定

该步骤发生在调用 ReactDOM.render 的过程中。在创建 fiberRoot 时,React 会在 root 节点的 DOM 元素上监听所有支持的事件。


function createRootImpl(
  container: Container,
  tag: RootTag,
  options: void | RootOptions,
) {
  // ...
  const rootContainerElement =
        container.nodeType === COMMENT_NODE ? container.parentNode : container;
  // 监听所有支持的事件
  listenToAllSupportedEvents(rootContainerElement);
  // ...
}

listenToAllSupportedEvents

在绑定事件时,React 通过名为 allNativeEvents 的 Set 变量获取对应的 eventName。该变量会在一个顶层函数中进行收集,而 nonDelegatedEvents 是一个预先定义好的 Set。


export function listenToAllSupportedEvents(rootContainerElement: EventTarget) {
  allNativeEvents.forEach(domEventName => {
    // 排除不需要委托的事件
    if (!nonDelegatedEvents.has(domEventName)) {
      // 冒泡
      listenToNativeEvent(
        domEventName,
        false,
        ((rootContainerElement: any): Element),
        null,
      );
    }
    // 捕获
    listenToNativeEvent(
      domEventName,
      true,
      ((rootContainerElement: any): Element),
      null,
    );
  });
}

listenToNativeEvent

在调用 listenToNativeEvent 函数绑定事件之前,React 会先在 DOM 元素中标记该事件名。只有当判断结果为 false 时,才会执行实际的绑定操作。


export function listenToNativeEvent(
  domEventName: DOMEventName,
  isCapturePhaseListener: boolean,
  rootContainerElement: EventTarget,
  targetElement: Element | null,
  eventSystemFlags?: EventSystemFlags = 0,
): void {
  let target = rootContainerElement;
	// ...
  // 在DOM元素上储存一个Set用来标识当前元素监听了那些事件
  const listenerSet = getEventListenerSet(target);
  // 事件的标识key,字符串拼接处理了下
  const listenerSetKey = getListenerSetKey(
    domEventName,
    isCapturePhaseListener,
  );

  if (!listenerSet.has(listenerSetKey)) {
    // 标记为捕获
    if (isCapturePhaseListener) {
      eventSystemFlags |= IS_CAPTURE_PHASE;
    }
    // 绑定事件
    addTrappedEventListener(
      target,
      domEventName,
      eventSystemFlags,
      isCapturePhaseListener,
    );
    // 添加到set
    listenerSet.add(listenerSetKey);
  }
}

addTrappedEventListener

addTrappedEventListener 函数通过事件名获取对应优先级的 listener 函数,并将其交由下层函数处理事件绑定。

这个 listener 函数是一个闭包函数,其内部可以访问 targetContainer、domEventName 和 eventSystemFlags 这三个变量。


function addTrappedEventListener(
  targetContainer: EventTarget,
  domEventName: DOMEventName,
  eventSystemFlags: EventSystemFlags,
  isCapturePhaseListener: boolean,
  isDeferredListenerForLegacyFBSupport?: boolean,
) {
  // 根据优先级取得对应listener
  let listener = createEventListenerWrapperWithPriority(
    targetContainer,
    domEventName,
    eventSystemFlags,
  );

  if (isCapturePhaseListener) {
    addEventCaptureListener(targetContainer, domEventName, listener);
  } else {
    addEventBubbleListener(targetContainer, domEventName, listener);
  }
}

addEventCaptureListener 函数和 addEventBubbleListener 函数的内部实现,实际上是调用原生的 target.addEventListener 来绑定事件。

这一步骤会循环遍历一个存有事件名的 Set,将每一个事件对应的处理函数绑定到 root 节点的 DOM 元素上。

不需要委托事件绑定

不需要委托的事件中包含了媒体元素相关的事件。


export const nonDelegatedEvents: Set<DOMEventName> = new Set([
  'cancel',
  'close',
  'invalid',
  'load',
  'scroll',
  'toggle',
  ...mediaEventTypes,
]);
export const mediaEventTypes: Array<DOMEventName> = [
  'abort',
  'canplay',
  'canplaythrough',
  'durationchange',
  'emptied',
  'encrypted',
  'ended',
  'error',
  'loadeddata',
  'loadedmetadata',
  'loadstart',
  'pause',
  'play',
  'playing',
  'progress',
  'ratechange',
  'seeked',
  'seeking',
  'stalled',
  'suspend',
  'timeupdate',
  'volumechange',
  'waiting',
];

setInitialProperties

在 setInitialProperties 方法中,React 会绑定不需要委托的事件到 DOM 元素本身,同时也会设置 style 和一些传入的 DOM 属性。


export function setInitialProperties(
  domElement: Element,
  tag: string,
  rawProps: Object,
  rootContainerElement: Element | Document,
): void {
  let props: Object;
  switch (tag) {
    // ...
    case 'video':
    case 'audio':
      for (let i = 0; i < mediaEventTypes.length; i++) {
        listenToNonDelegatedEvent(mediaEventTypes[i], domElement);
      }
      props = rawProps;
      break;
    default:
      props = rawProps;
  }
  // 设置DOM属性,如style...
  setInitialDOMProperties(
    tag,
    domElement,
    rootContainerElement,
    props,
    isCustomComponentTag,
  );
}

在 switch 语句中,React 会根据不同的元素类型绑定对应的事件。此处仅保留了 video 元素和 audio 元素的处理逻辑,它们会遍历 mediaEventTypes 将事件绑定在 DOM 元素本身上。

listenToNonDelegatedEvent

listenToNonDelegatedEvent 方法的逻辑与上一节的 listenToNativeEvent 方法基本一致。


export function listenToNonDelegatedEvent(
  domEventName: DOMEventName,
  targetElement: Element,
): void {
  const isCapturePhaseListener = false;
  const listenerSet = getEventListenerSet(targetElement);
  const listenerSetKey = getListenerSetKey(
    domEventName,
    isCapturePhaseListener,
  );
  if (!listenerSet.has(listenerSetKey)) {
    addTrappedEventListener(
      targetElement,
      domEventName,
      IS_NON_DELEGATED,
      isCapturePhaseListener,
    );
    listenerSet.add(listenerSetKey);
  }
}

值得注意的是,虽然事件处理函数绑定在 DOM 元素本身,但绑定的并非代码中传入的用户函数。后续触发时,React 仍会去收集处理函数并执行。

事件处理函数

这里的事件处理函数指的是 React 内部的默认处理函数,而非代码中传入的用户函数。

该函数通过 createEventListenerWrapperWithPriority 方法创建,对应的步骤在上文的 addTrappedEventListener 中。

createEventListenerWrapperWithPriority


export function createEventListenerWrapperWithPriority(
  targetContainer: EventTarget,
  domEventName: DOMEventName,
  eventSystemFlags: EventSystemFlags,
): Function {
  // 从内置的Map中获取事件优先级
  const eventPriority = getEventPriorityForPluginSystem(domEventName);
  let listenerWrapper;
  // 根据优先级不同返回不同的listener
  switch (eventPriority) {
    case DiscreteEvent:
      listenerWrapper = dispatchDiscreteEvent;
      break;
    case UserBlockingEvent:
      listenerWrapper = dispatchUserBlockingUpdate;
      break;
    case ContinuousEvent:
    default:
      listenerWrapper = dispatchEvent;
      break;
  }
  return listenerWrapper.bind(
    null,
    domEventName,
    eventSystemFlags,
    targetContainer,
  );
}

createEventListenerWrapperWithPriority 函数返回对应事件优先级的 listener,这三个函数均接收 4 个参数。


function fn(
  domEventName,
  eventSystemFlags,
  container,
  nativeEvent,
) {
  //...
}

在返回时,React 对前 3 个参数进行了 bind 操作,使得返回的函数只接收 nativeEvent 作为参数,但依然能访问前 3 个参数。

dispatchDiscreteEvent 方法和 dispatchUserBlockingUpdate 方法内部实际上都调用了 dispatchEvent 方法。

dispatchEvent

此处删除了大量代码,仅关注触发事件的核心代码。


export function dispatchEvent(
  domEventName: DOMEventName,
  eventSystemFlags: EventSystemFlags,
  targetContainer: EventTarget,
  nativeEvent: AnyNativeEvent,
): void {
  // ...
  // 触发事件
  attemptToDispatchEvent(
    domEventName,
    eventSystemFlags,
    targetContainer,
    nativeEvent,
  );
  // ...
}

attemptToDispatchEvent 方法中依然处理很多复杂逻辑,且函数调用栈有多层。此处跳过所有非关键逻辑,仅关注关键的触发函数。

dispatchEventsForPlugins

dispatchEventsForPlugins 函数负责收集触发事件开始各层级节点对应的处理函数,即我们在 JSX 中实际传入的函数,并执行它们。


function dispatchEventsForPlugins(
  domEventName: DOMEventName,
  eventSystemFlags: EventSystemFlags,
  nativeEvent: AnyNativeEvent,
  targetInst: null | Fiber,
  targetContainer: EventTarget,
): void {
  const nativeEventTarget = getEventTarget(nativeEvent);
  const dispatchQueue: DispatchQueue = [];
  // 收集listener模拟冒泡
  extractEvents(
    dispatchQueue,
    domEventName,
    targetInst,
    nativeEvent,
    nativeEventTarget,
    eventSystemFlags,
    targetContainer,
  );
  // 执行队列
  processDispatchQueue(dispatchQueue, eventSystemFlags);
}

extractEvents

extractEvents 函数主要针对不同类型的事件创建对应的合成事件,并将各层级节点的 listener 收集起来,用于模拟冒泡或捕获过程。

这段代码较长,已删除了不少无关代码。


function extractEvents(
  dispatchQueue: DispatchQueue,
  domEventName: DOMEventName,
  targetInst: null | Fiber,
  nativeEvent: AnyNativeEvent,
  nativeEventTarget: null | EventTarget,
  eventSystemFlags: EventSystemFlags,
  targetContainer: EventTarget,
): void {
  const reactName = topLevelEventsToReactNames.get(domEventName);
  let SyntheticEventCtor = SyntheticEvent;
  let reactEventType: string = domEventName;
	// 根据不同的事件来创建不同的合成事件
  switch (domEventName) {
    case 'keypress':
    case 'keydown':
    case 'keyup':
      SyntheticEventCtor = SyntheticKeyboardEvent;
      break;
    case 'click':
    // ...
    case 'mouseover':
      SyntheticEventCtor = SyntheticMouseEvent;
      break;
    case 'drag':
    // ...
    case 'drop':
      SyntheticEventCtor = SyntheticDragEvent;
      break;
    // ...
    default:
      break;
  }
  // ...
  // 收集各层级的listener
  const listeners = accumulateSinglePhaseListeners(
    targetInst,
    reactName,
    nativeEvent.type,
    inCapturePhase,
    accumulateTargetOnly,
  );
  if (listeners.length > 0) {
    // 创建合成事件
    const event = new SyntheticEventCtor(
      reactName,
      reactEventType,
      null,
      nativeEvent,
      nativeEventTarget,
    );
    dispatchQueue.push({event, listeners});
  }
}

accumulateSinglePhaseListeners

accumulateSinglePhaseListeners 函数通过向上层遍历来收集一个列表,该列表后续将用于模拟冒泡过程。


export function accumulateSinglePhaseListeners(
  targetFiber: Fiber | null,
  reactName: string | null,
  nativeEventType: string,
  inCapturePhase: boolean,
  accumulateTargetOnly: boolean,
): Array<DispatchListener> {
  const captureName = reactName !== null ? reactName + 'Capture' : null;
  const reactEventName = inCapturePhase ? captureName : reactName;
  const listeners: Array<DispatchListener> = [];

  let instance = targetFiber;
  let lastHostComponent = null;

  // 通过触发事件的fiber节点向上层遍历收集dom和listener
  while (instance !== null) {
    const {stateNode, tag} = instance;
    // 只有HostComponents有listener (i.e. <div>)
    if (tag === HostComponent && stateNode !== null) {
      lastHostComponent = stateNode;

      if (reactEventName !== null) {
        // 从fiber节点上的props中获取传入的事件listener函数
        const listener = getListener(instance, reactEventName);
        if (listener != null) {
          listeners.push({
            instance,
            listener,
            currentTarget: lastHostComponent,
          });
        }
      }
    }
    if (accumulateTargetOnly) {
      break;
    }
    // 继续向上
    instance = instance.return;
  }
  return listeners;
}

最后的数据结构如下:

dispatchQueue 的数据结构为数组,类型为 [{ event, listeners }]。

其中的 listeners 为一层一层收集到的数据,类型为 [{ currentTarget, instance, listener }]。

processDispatchQueue

processDispatchQueue 函数负责遍历 dispatchQueue。


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 中的每一项在 processDispatchQueueItemsInOrder 函数中被遍历执行。

processDispatchQueueItemsInOrder


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;
    }
  }
}

processDispatchQueueItemsInOrder 函数根据判断结果,通过正向和反向遍历来模拟冒泡和捕获过程。

executeDispatch

executeDispatch 函数负责执行具体的 listener。


function executeDispatch(
  event: ReactSyntheticEvent,
  listener: Function,
  currentTarget: EventTarget,
): void {
  const type = event.type || 'unknown-event';
  event.currentTarget = currentTarget;
  listener(event);
  event.currentTarget = null;
}

结语

本文旨在理清事件机制的执行流程,按照函数执行栈简单罗列了代码逻辑。如果不对照源码阅读,很难完全理解,其核心原理已在开篇讲述。

React 的事件机制隐晦而复杂,根据不同情况做了大量判断,并且还有优先级相关代码和合成事件等内容,这里并未一一讲解,原因当然是我还没看~

平时使用 React 主要是编写简单的手机页面,以前老板还经常吐槽加载不够快,那也没啥办法。就对我的工作而言,是否有 Concurrent 特性都无关紧要,这种复杂的合成事件机制完全是不需要的。不过 React 的作者们脑洞确实厉害,要是没看源码,我肯定想不到竟然模拟了一套事件机制。

小思考

  • 为什么原生事件的 stopPropagation 可以阻止合成事件的传递?

这些问题我以前根本没想过,不过今天看了源码后才产生的思考。

  • 因为合成事件是在原生事件触发之后才开始收集并触发的,所以当原生事件调用 stopPropagation 阻止传递后,事件根本到达不了 root 节点,也就触发不了 React 绑定的处理函数。自然合成事件也不会触发,所以原生事件不是阻止了合成事件的传递,而是阻止了 React 中绑定的事件函数的执行。

<div 原生onClick={(e)=>{e.stopPropagation()}}>
  <div onClick={()=>{console.log("合成事件")}}>合成事件</div>
</div>

比如这个例子,在原生 onClick 阻止传递后,控制台连“合成事件”这 4 个字都不会打出来了。

以上就是 React 事件机制源码解析的详细内容,更多关于 React 事件机制源码的资料请关注本站其它相关文章!

作者最新文章