React 事件源码解析

679 阅读8分钟

React 事件是在原生事件的基础上再封装了一层,且模拟实现了原生事件的捕获、冒泡阶段。我们可以将整体拆分为三个阶段来学习 事件的绑定事件的触发及收集回调函数执行

提示

  • 文章的源代码是基于 React 18.2.0
  • 文章内贴的源代码都只是保留关键部分,完整的代码可以根据代码位置自行查阅

事件的绑定

React 将事件分为可以委托的事件(可以冒泡的事件)和不可以委托的事件(不可以冒泡的事件)。可以委托的事绑定于 rootContainerElement(这个是 React 挂载的根元素)上,例如 click 事件;不可以委托的事件绑定于对应的元素上,例如 img 的 load 事件

对于可以委托的事件

对于可以冒泡的事件,在 createRoot 方法内,通过调用 listenToAllSupportedEvents 方法进行绑定

listenToAllSupportedEvents

/**
 * 代码位置 packages/react-dom/src/events/DOMPluginEventSystem.js
 */
export function listenToAllSupportedEvents(rootContainerElement: EventTarget) {
  ...
  // allNativeEvents保存了一系列的事件名称,当然也包括click事件,其成员可见simpleEventPluginEvents
  allNativeEvents.forEach((domEventName) => {
    // 将不可冒泡的事件排除在外
    if (!nonDelegatedEvents.has(domEventName)) {
      // 冒泡阶段的事件监听
      listenToNativeEvent(domEventName, false, rootContainerElement);
    }
    // 捕获阶段的事件监听
    listenToNativeEvent(domEventName, true, rootContainerElement);
  });
  ...
}

listenToNativeEvent

/**
 * 代码位置 packages/react-dom/src/events/DOMPluginEventSystem.js
 */
export function listenToNativeEvent(
  domEventName: DOMEventName,
  isCapturePhaseListener: boolean,
  target: EventTarget
): void {
  ...
  let eventSystemFlags = 0;
  // 添加捕获事件标识
  if (isCapturePhaseListener) {
    eventSystemFlags |= IS_CAPTURE_PHASE;
  }

  addTrappedEventListener(
    target,
    domEventName,
    eventSystemFlags,
    isCapturePhaseListener
  );
  ...
}

addTrappedEventListener

addTrappedEventListener 是事件绑定的核心方法,后续的不可以冒泡事件也是由此方法进行绑定

/**
 * 代码位置 packages/react-dom/src/events/DOMPluginEventSystem.js
 */
function addTrappedEventListener(
  targetContainer: EventTarget,
  domEventName: DOMEventName,
  eventSystemFlags: EventSystemFlags,
  isCapturePhaseListener: boolean,
  isDeferredListenerForLegacyFBSupport?: boolean
) {
  ...
  // 这个才是绑定到DOM元素上的事件处理函数
  let listener = createEventListenerWrapperWithPriority(
    targetContainer,
    domEventName,
    eventSystemFlags
  );

  if (isCapturePhaseListener) {
    // 捕获阶段的事件监听
    unsubscribeListener = addEventCaptureListener(
      targetContainer,
      domEventName,
      listener
    );
  } else {
    // 冒泡阶段的事件监听
    unsubscribeListener = addEventBubbleListener(
      targetContainer,
      domEventName,
      listener
    );
  }
...
}
  • addEventCaptureListener、addEventBubbleListener 内部都是调用的原生的 addEventListener,只是针对捕获、冒泡阶段及 passive 传递参数不同
  • 这里的 listener 才是元素触发事件时的回调函数(而非我们在 JSX 中写入的事件回调函数),即当 rootContainerElement 触发事件时(例如 click 事件),就会执行该函数

至此针对可冒泡事件就绑定完毕了

对于不可冒泡事件

对于不可冒泡事件,在 reconcile 的 completeWork 阶段,执行 finalizeInitialChildren 方法进行绑定

finalizeInitialChildren

/*
 * 代码位置 packages/react-dom/src/client/ReactDOMHostConfig.js
 */
export function finalizeInitialChildren(
  domElement: Instance,
  type: string,
  props: Props,
  rootContainerInstance: Container,
  hostContext: HostContext
): boolean {
  ...
  setInitialProperties(domElement, type, props, rootContainerInstance);
  ...
}

setInitialProperties

根据不同元素,来绑定不同的事件

  • listenToNonDelegatedEvent 最终也是通过 addTrappedEventListener 方法来为元素绑定事件,addTrappedEventListener 方法在上面已经介绍过了
  • 可以看到此时是没有判断 props 传值来绑定事件的,表示即使你在书写 JSX 时 <img> 标签即使不传入 onLoad 等等,也是会绑定 load 事件的
/*
 * 代码位置 packages/react-dom/src/client/ReactDOMComponent.js
 */
export function setInitialProperties(
  domElement: Element,
  tag: string,
  rawProps: Object,
  rootContainerElement: Element | Document | DocumentFragment
): void {
  ...
  // 根据不同的标签,来绑定不同的事件
  switch (tag) {
    case 'dialog':
      listenToNonDelegatedEvent('cancel', domElement);
      listenToNonDelegatedEvent('close', domElement);
      props = rawProps;
      break;

    case 'img':
    case 'image':
    case 'link':
      listenToNonDelegatedEvent('error', domElement);
      listenToNonDelegatedEvent('load', domElement);
      props = rawProps;
      break;
  }
  ...
}

事件的触发及依赖收集

当元素触发事件后,对于可以冒泡的事件,即冒泡到 rootContainerElement 后触发事件,并执行回调;对于不可冒泡事件,则直接执行回调。步骤如下

  1. 通过 JS 原生事件的 event.target 找到触发的元素,并根据元素获取到对应的 Fiber 节点
  2. 找到对应的 Fiber 节点后,向上遍历,收集事件 & 事件处理函数(JSX 中传入的事件回调),插入队列中
  3. 当收集完毕后,执行刚刚收集的事件处理函数

假设我们先有如下组件,绑定了如下事件,并最终用鼠标点击了一次 img 元素

function App() {
  return (
    <div onClick={() => console.log('click div')}>
      <img src="xx"
        onLoad={() => { console.log('load img') }}
        onClick={() => console.log('click image')}
      />
    </div>
  )
}
}

元素触发事件并执行回调函数

我们刚刚点击了一下 img 元素,当事件进入冒泡阶段时, rootContainerElement 触发 click 事件,并执行由 addTrappedEventListener 绑定 listener。listener 由 createEventListenerWrapperWithPriority 创建而得

createEventListenerWrapperWithPriority

/**
 * 代码位置 packages/react-dom/src/events/ReactDOMEventListener.js
 */
export function createEventListenerWrapperWithPriority(
  targetContainer: EventTarget, // rootContainerElement
  domEventName: DOMEventName, // click
  eventSystemFlags: EventSystemFlags // 0
): Function {
  ...
  const eventPriority = getEventPriority(domEventName);
  let listenerWrapper;

  // 区分优先级
  switch (eventPriority) {
    case DiscreteEventPriority:
      listenerWrapper = dispatchDiscreteEvent;
      break;
    case ContinuousEventPriority:
      listenerWrapper = dispatchContinuousEvent;
      break;
    case DefaultEventPriority:
    default:
      listenerWrapper = dispatchEvent;
      break;
  }
  return listenerWrapper.bind(
    null,
    domEventName,
    eventSystemFlags,
    targetContainer
  );
  ...
}
  • 这里进行了优先级的判断,会根据事件的名称来赋予不同的优先级,例如 click 事件,他的优先级就是 SyncLane,除去优先级,其内部调用的方法相同,最终都是调用的 dispatchEvent

dispatchEventWithEnableCapturePhaseSelectiveHydrationWithoutDiscreteEventReplay

dispatchEvent 方法比较简单,所以没有列出,其核心就是调用 dispatchEventWithEnableCapturePhaseSelectiveHydrationWithoutDiscreteEventReplay 方法

/**
 * 代码位置 packages/react-dom/src/events/ReactDOMEventListener.js
 */
function dispatchEventWithEnableCapturePhaseSelectiveHydrationWithoutDiscreteEventReplay(
  domEventName: DOMEventName,
  eventSystemFlags: EventSystemFlags,
  targetContainer: EventTarget,
  nativeEvent: AnyNativeEvent
) {
  ...
  // 找到 event.target 所对应的 Fiber
  let blockedOn = findInstanceBlockingEvent(
    domEventName,
    eventSystemFlags,
    targetContainer,
    nativeEvent
  );

  if (blockedOn === null) {
    // 执行依赖收集函数
    dispatchEventForPluginEventSystem(
      domEventName,
      eventSystemFlags,
      nativeEvent,
      return_targetInst,
      targetContainer
    );
  }
  ...
}
  • 在每个 Fiber 中,我们通过 startNode 来可以访问对用的 DOM。当然在对应的 DOM 中,我们也可以通过变量 internalInstanceKey 来访问对应的 Fiber,由此我们通过 event.target 找到触发当前事件的 Fiber。此次 event.target 表示的 img 元素,return_targetInst 表示的 img 对应的 Fiber

收集事件 & 事件处理函数

dispatchEventForPluginEventSystem 方法比较简单,所以没有列出,其核心就是调用了 dispatchEventsForPlugins 方法

dispatchEventsForPlugins

/**
 * 代码位置 packages/react-dom/src/events/DOMPluginEventSystem.js
 */
function dispatchEventsForPlugins(
  domEventName: DOMEventName,
  eventSystemFlags: EventSystemFlags,
  nativeEvent: AnyNativeEvent,
  targetInst: null | Fiber,
  targetContainer: EventTarget
): void {
  ...
  const nativeEventTarget = getEventTarget(nativeEvent);

  // 存储的事件 & 事件处理函数的队列
  const dispatchQueue: DispatchQueue = [];

  // 根据当前Fiber去收集事件 & 事件处理函数
  extractEvents(
    dispatchQueue,
    domEventName,
    targetInst,
    nativeEvent,
    nativeEventTarget,
    eventSystemFlags,
    targetContainer
  );

  // 依赖收集完毕后开始执行,后续再说
  processDispatchQueue(dispatchQueue, eventSystemFlags);
}

extractEvents

extractEvents 方法内核心是调用了 SimpleEventPlugin.extractEvents,所以没有单独列出 extractEvents 的源代码

/**
 *代码位置 packages/react-dom/src/events/plugins/SimpleEventPlugin.js
 */
function extractEvents(
  dispatchQueue: DispatchQueue,
  domEventName: DOMEventName,
  targetInst: null | Fiber,
  nativeEvent: AnyNativeEvent,
  nativeEventTarget: null | EventTarget,
  eventSystemFlags: EventSystemFlags,
  targetContainer: EventTarget
) {
  ...
  // 收集事件,并对事件进行封装,以及其他兼容性的处理,这个也就是后续我们在 JSX 回调函数中可以获取到的事件
  let SyntheticEventCtor = SyntheticEvent;
  switch (domEventName) {
    case 'click':
      SyntheticEventCtor = SyntheticMouseEvent;
      break;
  }

  // 收集事件的处理函数
  const listeners = accumulateSinglePhaseListeners(
    targetInst,
    reactName,
    nativeEvent.type,
    inCapturePhase,
    accumulateTargetOnly,
    nativeEvent
  );

  if (listeners.length > 0) {
    const event = new SyntheticEventCtor(
      reactName,
      reactEventType,
      null,
      nativeEvent,
      nativeEventTarget
    );

    // 依赖收集完毕后,插入队列中
    dispatchQueue.push({ event, listeners });
  }
  ...
}

accumulateSinglePhaseListeners

/**
 * 代码位置 packages/react-dom/src/events/DOMPluginEventSystem.js
 */
export function accumulateSinglePhaseListeners(
  targetFiber: Fiber | null,
  reactName: string | null,
  nativeEventType: string,
  inCapturePhase: boolean,
  accumulateTargetOnly: boolean,
  nativeEvent: AnyNativeEvent
): Array<DispatchListener> {
  ...
  const captureName = reactName !== null ? reactName + 'Capture' : null;
  const reactEventName = inCapturePhase ? captureName : reactName;

  let listeners: Array<DispatchListener> = [];
  let instance = targetFiber;

  while (instance !== null) {
    const { stateNode, tag } = instance;

    // Fiber 类型必须是 HostComponent,例如 div,img
    if (tag === HostComponent && stateNode !== null) {
      if (reactEventName !== null) {
        // 通过 reactEventName 去匹配该 Fiber 的 props,匹配成功后,即可获得事件的执行函数
        const listener = getListener(instance, reactEventName);
        if (listener != null) {
          listeners.push(
            createDispatchListener(instance, listener, lastHostComponent)
          );
        }
      }
    }
    ...

    // 向父节点搜索
    instance = instance.return;
  }
  return listeners;
}
  • 收集事件的处理函数比较清晰,概括来说就是从 targetFiber 开始向上搜索,拿事件的名称去匹配每个 Fiber 的 Props。拿此次的例子举例,此次的事件名称为 click,则计算出 reactEventName 为 onClick(如果是捕获阶段的话为 onClickCapture),img 的 Fiber 存在如下 props { onClick: () => console.log('click image') },即此次匹配成功,会将收集到的事件和事件处理函数存储队列中

事件处理函数执行

回到 dispatchEventsForPlugins 函数,当收集完毕后,就开始执行收集的任务了

/**
 * 代码位置 packages/react-dom/src/events/DOMPluginEventSystem.js
 */
function dispatchEventsForPlugins(
  domEventName: DOMEventName,
  eventSystemFlags: EventSystemFlags,
  nativeEvent: AnyNativeEvent,
  targetInst: null | Fiber,
  targetContainer: EventTarget
): void {
  const nativeEventTarget = getEventTarget(nativeEvent);

  // 存储的事件 & 事件处理函数的队列
  const dispatchQueue: DispatchQueue = [];

  // 根据当前Fiber去收集事件 & 事件处理函数
  extractEvents(
    dispatchQueue,
    domEventName,
    targetInst,
    nativeEvent,
    nativeEventTarget,
    eventSystemFlags,
    targetContainer
  );

  // 依赖收集完毕后开始执行
  processDispatchQueue(dispatchQueue, eventSystemFlags);
}

processDispatchQueue

/**
 *代码位置 packages/react-dom/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);
    //  event system doesn't use pooling.
  }
  // This would be a good time to rethrow if any of the event handlers threw.
  rethrowCaughtError();
}

processDispatchQueueItemsInOrder

/**
 *代码位置 packages/react-dom/src/events/DOMPluginEventSystem.js
 */
function processDispatchQueueItemsInOrder(
  event: ReactSyntheticEvent,
  dispatchListeners: Array<DispatchListener>,
  inCapturePhase: boolean
): void {
  ...
  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];

      // 这里调用了 isPropagationStopped,如果事件在冒泡时,执行力event.stopPropagation(),则此函数返回为true,后续的listener也不会执行了
      if (instance !== previousInstance && event.isPropagationStopped()) {
        return;
      }

      // 执行事件的回到函数,此时才是真正执行 JSX 中传递的事件回调函数
      executeDispatch(event, listener, currentTarget);
      previousInstance = instance;
    }
  }
  ...
}
  • React 进行收集时的顺序是从 子 Fiber -> 根 Fiber,所以存储在队列中的事件执行函数顺序也是如此,即顺序执行就可以模拟冒泡阶段,逆序执行就可以模拟捕获阶段

总结

  1. React 将事件分为可以委托的事件(可以冒泡的事件)和不可以委托的事件(不可以冒泡的事件)。可以委托的事件在 createRoot 段绑定于 rootContainerElement;不可以委托的事件在 completeWork 段绑定于对应的元素上
  2. 当一个元素触发事件后,对于可以冒泡的事件,即冒泡到 rootContainerElement 后触发事件,并执行回调;对于不可冒泡事件,则直接执行回调。捕获事件和冒泡事件执行步骤相似,只是捕获先于冒泡执行,也就是说,步骤 3,4 是会执行两次的,一次是捕获,一次是冒泡
  3. 执行回调时,通过 JS 原生事件的 event.target 找到触发的元素,并根据元素获取到对应的 Fiber 节点,找到对应的 Fiber 节点后,向上遍历,收集事件 & 事件处理函数(JSX 中传入的事件回调),插入队列中
  4. 收集完毕后,根据不同阶段,捕获阶段从后往前执行,冒泡节点从前往后执行,依次清空队列

思考

  1. 子元素如此绑定 <div onClick={(event) => { event.stopPropagation() }}></div> 后,父元素通过 ref.current.addEventListener 还可以监听到吗
  2. 子元素通过 ref.current.addEventListener 内 event.stopPropagation() 后,父元素如此绑定 <div onClick={(event) => { event.stopPropagation() }}></div>还可以监听到吗