React事件原理梳理

316 阅读16分钟

前言

最近阅读了卡颂老师60行代码实现React的事件系统, 实现了简单模拟React事件系统 demo。后面自己也看了相关源码和博客,梳理一下相关的事件原理。

React 中有自己的事件系统模式,通常被称为 React合成事件。之所以采用这种自定义的合成事件,一方面是为了抹平浏览器差异性,使得 React 开发者不再需要去关注浏览器事件兼容性问题,另一方面是为了统一管理事件,提高性能,这主要体现在 React 内部实现事件委托,并且记录当前事件发生的状态上。

先了解几个概念:

什么是DOM事件?

DOM事件模型是W3C制定的标准模型,现代浏览器(除IE6-8之外的浏览器)都支持该模型,正常DOM事件有3个过程:

  • 事件捕获阶段(capturing phase):

    • 事件从document一直向下传播到目标元素, 依次检查经过的节点是否绑定了事件监听函数,如果有则执行。

    • React如果想要在捕获阶段执行可以将事件后面加上 Capture 后缀,比如 onClickCapture,onChangeCapture。

  • 事件处理阶段(target phase):

    • 事件到达目标元素, 触发目标元素的监听函数
  • 事件冒泡阶段(bubbling phase):

    • 事件从目标元素冒泡到document, 依次检查经过的节点是否绑定了事件监听函数,如果有则执行。

    • 开发者正常给 React 绑定的事件比如 onClick,onChange,默认会在模拟冒泡阶段执行。

    • 阻止冒泡 e.stopPropagation()、阻止默认行为e.preventDefault() 都是React 单独处理的

什么是事件委托/事件代理?

  • 事件委托的概念

事件委托又称事件代理。是指将自身的事件委托给上级处理。即:子级将事件委托给父级来处理。

  • 事件委托的实现原理

事件委托就是利用事件冒泡,只指定一个事件处理程序,就可以管理某一类型的所有事件。

这种机制不会把事件处理函数直接绑定在真实的节点上,而是把所有的事件绑定到结构的最外层,使用一个统一的事件监听和处理函数。当组件加载或卸载时,只是在这个统一的事件监听器上插入或删除一些对象,当事件发生时,首先被这个统一的事件监听器处理,延后在映射表里找到真正的事件处理函数并调用。

React事件系统原理

React事件系统可以分为三个部分

  • 事件注册:registerEvents
  • 事件绑定:listenToAllSupportedEvents
  • 事件派发/合成:dispatchEvent/ SyntheticBaseEvent

下图是梳理的总体流程图,接下来从源码中看下事件的处理流程。

react事件系统.drawio.png

事件绑定(注册)

  1. 入口

源码的入口程序在 react-dom/src/events/DOMPluginEventSystem.js#L89-L93

React通过插件注册事件,建立原生DOM事件名称与React事件名称映射关系

SimpleEventPlugin 是合成事件系统的基本功能实现,而其他的几个 EventPlugin 只不过是它的 polyfill,在没有这些 polyfill 插件的情况下可允许发布 React 的构建

// packages/react-dom/src/events/DOMPluginEventSystem.js
SimpleEventPlugin.registerEvents();
EnterLeaveEventPlugin.registerEvents();
ChangeEventPlugin.registerEvents();
SelectEventPlugin.registerEvents();
BeforeInputEventPlugin.registerEvents();
  1. 事件优先级

SimpleEventPlugin.registerEvents 这里会去调用registerSimpleEvents,这里会注册大部分事件,它们在React被定义为顶级事件。React将这些事件分3个优先级

// 离散事件,cancel、click、mousedown、keyup、change 这类单点触发不持续的事件,优先级最低
export const DiscreteEvent: EventPriority = 0; 
// 用户阻塞事件,drag、mousemove、wheel、scroll 这类持续触发的事件,优先级相对较高
export const UserBlockingEvent: EventPriority = 1; 
// 连续事件,load、error、waiting 这类大多与媒体相关的事件为主的事件需要及时响应,所以优先级最高
export const ContinuousEvent: EventPriority = 2; 

3.registerEvents本质上调用的是registerSimpleEvents

// DOMEventProperties.js
export function registerSimpleEvents() {
  registerSimplePluginEventsAndSetTheirPriorities(
    discreteEventPairsForSimpleEventPlugin,
    DiscreteEvent,
  );
  registerSimplePluginEventsAndSetTheirPriorities(
    userBlockingPairsForSimpleEventPlugin,
    UserBlockingEvent,
  );
  registerSimplePluginEventsAndSetTheirPriorities(
    continuousPairsForSimpleEventPlugin,
    ContinuousEvent,
  );
  setEventPriorities(otherDiscreteEvents, DiscreteEvent);
}

// 注册事件和优先级
function registerSimplePluginEventsAndSetTheirPriorities(
  eventTypes: Array<DOMEventName | string>,
  priority: EventPriority,
): void {
  for (let i = 0; i < eventTypes.length; i += 2) {
    const topEvent = ((eventTypes[i]: any): DOMEventName);
    const event = ((eventTypes[i + 1]: any): string);
    // 将事件名更名为react事件名如click -- onClick
    const capitalizedEvent = event[0].toUpperCase() + event.slice(1);
    const reactName = 'on' + capitalizedEvent;
    eventPriorities.set(topEvent, priority);
    topLevelEventsToReactNames.set(topEvent, reactName);
    registerTwoPhaseEvent(reactName, [topEvent]);
  }
}
  1. registerTwoPhaseEvent 注册捕获和冒泡两个阶段的事件
  • 把react事件名称跟对应的dependencies关系存到registrationNameDependencies中:维护一个数据结构 {'onclick':['click'],… }
  • 把所有依赖存到allNativeEvents(所有有意义的原生事件名称的Set集合)中
// EventRegistry.js
export function registerTwoPhaseEvent(
  registrationName: string,
  dependencies: Array<DOMEventName>,
): void {
  registerDirectEvent(registrationName, dependencies);
  registerDirectEvent(registrationName + 'Capture', dependencies);
}

export function registerDirectEvent(
  registrationName: string,
  dependencies: Array<DOMEventName>,
) {
  // ...
  registrationNameDependencies[registrationName] = dependencies;
  // ...
  for (let i = 0; i < dependencies.length; i++) {
    / /生成allNativeEvents对象
    allNativeEvents.add(dependencies[i]);
  }
}

  1. 总结下事件注册的流程图 image.png

事件监听

  1. 入口

在 17.x 版本中,创建 ReactRoot 阶段便会调用 listenToAllSupportedEvents 函数,并在所有可以监听的原生事件上添加监听事件。 在 v17 之前是绑定在 document 上的,在 v17 改成了 app 容器上。这样更利于一个 html 下存在多个应用。

image.png

// packages/react-dom/src/client/ReactDOMRoot.js
// 在根DOM容器(div#root)上监听事件
const rootContainerElement = container.nodeType === COMMENT_NODE ? container.parentNode : container;
listenToAllSupportedEvents(rootContainerElement);
  1. listenToAllSupportedEvents

注:react的事件体系, 不是全部都通过事件委托来实现的,有一些特殊情况, 是直接绑定到对应 DOM 元素上的(如:scroll, load), 它们都通过listenToNonDelegatedEvent函数进行绑定

// packages/react-dom/src/events/DOMPluginEventSystem.js
// 入参 rootContainerElement 由创建 ReactRoot 的函数传入,其内容为 React 应用的根 DOM 节点。
export function listenToAllSupportedEvents(rootContainerElement: EventTarget) {
  // enableEagerRootListeners 为固定不变的标识常量,常为 true,可忽略。
  // 其意义是指“尽早的在所有原生事件上添加监听器”这一特性是否开启,与之相对的在 16.x 版本中监听器会在较晚的时机按需添加。
  if (enableEagerRootListeners) {
    // listeningMarker 是一个由固定字符加随机字符组成的标识,用于标识节点是否已经以 react 的方式在所有原生事件上添加监听事件,
    // 如果已经添加过,则直接跳过,节省一些不必要的工作
    if ((rootContainerElement: any)[listeningMarker]) {
      return;
    }  
    // 添加标识
    (rootContainerElement: any)[listeningMarker] = true;   
    // 遍历所有原生事件
    // 除了不需要在冒泡阶段添加事件代理的原生事件,仅在捕获阶段添加事件代理
    // 其余的事件都需要在捕获、冒泡阶段添加代理事件
    allNativeEvents.forEach(domEventName => {
      if (!nonDelegatedEvents.has(domEventName)) {
          // nonDelegatedEvents 存储着不会在DOM中冒泡的事件,如视频元素上的事件,scroll、load 事件等
          // 这里将会冒泡到DOM上的事件绑定到根DOM容器上
          // listenToNativeEvent 的第二个参入传入 false ,表示在冒泡阶段监听事件
        listenToNativeEvent(
          domEventName,
          false,
          ((rootContainerElement: any): Element),
          null,
        );
      }
      // 这里将不会在DOM中冒泡的事件绑定在目标元素上
      // listenToNativeEvent 的第二个参入传入 true ,表示在捕获阶段监听事件
      listenToNativeEvent(
        domEventName,
        true,
        ((rootContainerElement: any): Element),
        null,
      );
    });
  }
}

无论是在捕获阶段还是在冒泡阶段注册事件监听,都是调用 listenToNativeEvent 函数,接下来看看这个函数。

  1. listenToNativeEvent
// packages/react-dom/src/events/DOMPluginEventSystem.js
export function listenToNativeEvent(
  domEventName: DOMEventName,  // 事件名称
  isCapturePhaseListener: boolean, // 是在捕获阶段还是冒泡阶段监听事件
  target: EventTarget, // 目标事件对象
): void {
  // 删除了Dev部分的代码
  let eventSystemFlags = 0;
  if (isCapturePhaseListener) {
    // IS_CAPTURE_PHASE  标记事件是在捕获阶段
    eventSystemFlags |= IS_CAPTURE_PHASE;
  }
  // 注册事件监听
  addTrappedEventListener(
    target,  // 目标事件对象
    domEventName, // 事件名称
    eventSystemFlags, // 
    isCapturePhaseListener, // boolean 值,false 表示在冒泡阶段监听事件,true 表示在捕获阶段监听事件 
  );
}

listenToNativeEvent 通过位运算给 eventSystemFlags 变量添加了一个 IS_CAPTURE_PHASE 的标记,然后调用 addTrappedEventListener 函数来注册事件监听。

  1. addTrappedEventListener
// packages/react-dom/src/events/DOMPluginEventSystem.js
function addTrappedEventListener(
  targetContainer: EventTarget, // 目标事件对象
  domEventName: DOMEventName, // 目标事件名称
  eventSystemFlags: EventSystemFlags,
  isCapturePhaseListener: boolean,  // false 表示在冒泡阶段监听事件,true 表示在捕获阶段监听事件 
  isDeferredListenerForLegacyFBSupport?: boolean,
) {
  // 创建带有优先级的事件监听器,具体内容后面概述
  let listener = createEventListenerWrapperWithPriority(
    targetContainer,
    domEventName,
    eventSystemFlags,
  );
  // ...
  let unsubscribeListener;
  // 在原生事件上分别在捕获阶段和冒泡阶段注册事件监听器
  if (isCapturePhaseListener) {
    // ...
    unsubscribeListener = addEventCaptureListener(
      targetContainer,
      domEventName,
      listener,
    );
  } else {
    // ...
    unsubscribeListener = addEventBubbleListener(
      targetContainer,
      domEventName,
      listener,
    );
  }
}

addTrappedEventListener主要做两件事

  • 创建带有优先级的事件监听器
  • 在原生事件上分别在捕获阶段和冒泡阶段注册事件监听器
// packages/react-dom/src/events/ReactDOMEventListener.js
export function createEventListenerWrapperWithPriority(
  targetContainer: EventTarget,
  domEventName: DOMEventName,
  eventSystemFlags: EventSystemFlags,
): Function {
  // 根据事件名称获取事件的优先级
  const eventPriority = getEventPriorityForPluginSystem(domEventName);
  let listenerWrapper;
  // 根据事件优先级返回对应的事件监听函数
  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,
  );
}

把事件挂载到target上

export function addEventBubbleListener(
  target: EventTarget,
  eventType: string,
  listener: Function,
): Function {
  target.addEventListener(eventType, listener, false);
  return listener;
}

export function addEventCaptureListener(
  target: EventTarget,
  eventType: string,
  listener: Function,
): Function {
  target.addEventListener(eventType, listener, true);
  return listener;
}

最终在根 DOM 节点上,每个原生事件都绑定了其对应优先级所对应的 监听器

  1. 流程图

image.png

事件派发

当我们在页面点击按钮之后。以 onClick 为例子,率先触发的一定是挂载在根 DOM 节点上的 click 事件的监听器,也就是 dispatchDiscreteEvent

  • 离散事件监听器dispatchDiscreteEvent

  • 用户阻塞事件监听器dispatchUserBlockingUpdate

  • 连续事件或其他事件监听器dispatchEvent

和事件注册一样,listener也分为dispatchDiscreteEvent, dispatchUserBlockingUpdate, dispatchEvent三种。它们之间的主要区别是执行优先级,还有discreteEvent涉及到要清除之前的discreteEvent问题,所以做了区分。但是它们最后都会调用dispatchEvent

  1. dispatchDiscreteEvent (离散事件监听器)
// packages/react-dom/src/events/ReactDOMEventListener.js
function dispatchDiscreteEvent(
  domEventName,
  eventSystemFlags,
  container,
  nativeEvent,
) {
  // flushDiscreteUpdatesIfNeeded 的作用是清除先前积攒的为执行的离散任务,包括但不限于之前触发的离散事件和 useEffect 的回调,
  // 主要为了保证当前离散事件所对应的状态时最新的
  if (
    !enableLegacyFBSupport ||
    (eventSystemFlags & IS_LEGACY_FB_SUPPORT_MODE) === 0
  ) {
    flushDiscreteUpdatesIfNeeded(nativeEvent.timeStamp);
  }
  // 新建一个离散更新
  // 提前讲解一下它的入参,后面四个参数实际上第一个函数参数的参数
  // 后面会这么调用,dispatchEvent(domEventName, eventSystemFlags, container, nativeEvent)
  discreteUpdates(
    dispatchEvent,
    domEventName,
    eventSystemFlags,
    container,
    nativeEvent,
  );
}

// packages/react-dom/src/events/DOMPluginEventSystem.js
export function discreteUpdates(fn, a, b, c, d) {
  // 标记当前正在事件处理过程中,并存储之前的状态
  const prevIsInsideEventHandler = isInsideEventHandler;
  isInsideEventHandler = true;
  try {
    // 调用 Scheduler 里的离散更新函数
    return discreteUpdatesImpl(fn, a, b, c, d);
  } finally {
    // 如果之前就处于事件处理过程中,则继续完成
    isInsideEventHandler = prevIsInsideEventHandler;
    if (!isInsideEventHandler) {
      finishEventHandler();
    }
  }
}

let discreteUpdatesImpl = function(fn, a, b, c, d) {
  return fn(a, b, c, d);
};

  1. dispatchUserBlockingUpdate(用户阻塞事件监听器)
// packages/react-dom/src/events/DOMPluginEventSystem.js
// 前三个参数是在注册事件代理的时候便传入的,
// domEventName:对应原生事件名称
// eventSystemFlags:本文范文内其值仅有可能为 4 或者 0,分别代表 捕获阶段事件 和 冒泡阶段事件
// container:应用根 DOM 节点
// nativeEvent:原生监听器传入的 Event 对象
function dispatchUserBlockingUpdate(domEventName, eventSystemFlags, container, nativeEvent) {
  // 在React内部替换runWithPriority
  if (decoupleUpdatePriorityFromScheduler) {
    const previousPriority = getCurrentUpdateLanePriority();
    try {
      setCurrentUpdateLanePriority(InputContinuousLanePriority);
      runWithPriority(
        UserBlockingPriority,
        dispatchEvent.bind(null, domEventName, eventSystemFlags, container, nativeEvent ),
      );
    } finally {
      setCurrentUpdateLanePriority(previousPriority);
    }
  } else {
    runWithPriority(
      UserBlockingPriority,
      dispatchEvent.bind(null, domEventName, eventSystemFlags, container, nativeEvent ),
    );
  }
}
  1. dispatchEvent

事件派发的角色应该是dispatchEvent,核心是两块

  • attemptToDispatchEvent函数是尝试调度事件,如果调度事件失败,则返回 SuspenseInstance 或 根DOM容器。

    • 定位触发事件的原生DOM节点
    • 获取与原生DOM节点对应的fiber节点
    • 通过上面两步,将原生事件和 fiber树关联了起来。
    • 通过事件插件系统,派发事件(dispatchEventForPluginEventSystem)
  • dispatchEventForPluginEventSystem函数的作用,是通过React的事件插件系统来派发事件。

    • 作用是通过事件插件系统派发事件。在该函数中,为了兼容16以下的版本,会把click事件监听器注册到 document 上。然后从当前触发事件的DOM节点开始,向上遍历fiber树,找到根DOM容器,最后调用dispatchEventsForPlugins函数来派发事件
// packages/react-dom/src/events/ReactDOMEventListener.js
export function dispatchEvent(
  domEventName: DOMEventName, //  DOM事件名称,如:click,不是onClick;
  eventSystemFlags: EventSystemFlags, // 事件系统标记;
  targetContainer: EventTarget, // id=root的DOM元素;
  nativeEvent: AnyNativeEvent // 原生事件(来自addEventListener);
): void {
  if (!_enabled) {
    return;
  }
  const allowReplay = (eventSystemFlags & IS_CAPTURE_PHASE) === 0;
  if (
    allowReplay &&
    hasQueuedDiscreteEvents() &&
    isDiscreteEventThatRequiresHydration(domEventName)
  ) {
    // 按顺序发送离散事件 (离散事件(DiscreteEvent):click、keydown、focusin等,这些事件的触发不是连续的,优先级最高)
    // queueDiscreteEvent 将会创建一个可重播事件,并将其添加到「要重播的DiscreteEvent队列」中
    queueDiscreteEvent(null, domEventName, eventSystemFlags, targetContainer, nativeEvent);
    return;
  }

  // 尝试调度事件,如果被阻止,则返回 SuspenseInstance 或 Container
  let blockedOn = attemptToDispatchEvent(domEventName, eventSystemFlags, targetContainer, nativeEvent);

  if (blockedOn === null) {
    if (allowReplay) {
      // 将连续触发类型的事件重置为无事件
      clearIfContinuousEvent(domEventName, nativeEvent);
    }
    return;
  }

  if (allowReplay) {
    if (
      !enableCapturePhaseSelectiveHydrationWithoutDiscreteEventReplay &&
      isDiscreteEventThatRequiresHydration(domEventName)
    ) {
      // queueDiscreteEvent 将会创建一个可重播事件,并将其添加到「要重播的DiscreteEvent队列」中
      queueDiscreteEvent(blockedOn, domEventName, eventSystemFlags, targetContainer, nativeEvent);
      return;
    }
    if (
      queueIfContinuousEvent(blockedOn, domEventName, eventSystemFlags, targetContainer, nativeEvent)){
      return;
    }
    // 将连续触发类型的事件重置为无事件
    clearIfContinuousEvent(domEventName, nativeEvent);
  }

  if (
    enableCapturePhaseSelectiveHydrationWithoutDiscreteEventReplay &&
    eventSystemFlags & IS_CAPTURE_PHASE &&
    isDiscreteEventThatRequiresHydration(domEventName)
  ) {
    while (blockedOn !== null) {
      const fiber = getInstanceFromNode(blockedOn);
      if (fiber !== null) {
        attemptSynchronousHydration(fiber);
      }

      // 尝试调度事件,如果被阻止,则返回 SuspenseInstance 或 Container
      const nextBlockedOn = attemptToDispatchEvent(domEventName, eventSystemFlags, targetContainer, nativeEvent);
      if (nextBlockedOn === blockedOn) {
        break;
      }
      blockedOn = nextBlockedOn;
    }
    if (blockedOn) {
      // 执行原生事件的 stopPropagation 方法,阻止事件在捕获阶段冒泡阶段进一步传播 (传播意味着向上冒泡到父元素或向下捕获到子元素)
      nativeEvent.stopPropagation();
      return;
    }
  }

  // 通过插件系统,派发事件
  dispatchEventForPluginEventSystem(domEventName, eventSystemFlags, nativeEvent, null, targetContainer);
}
  1. attemptToDispatchEvent

attemptToDispatchEvent中, 根据nativeEventTarget找到真正触发事件的DOM元素,并根据DOM元素找到对应的fiber节点,判断fiber节点的类型以及是否已渲染来决定是否要派发事件。

// packages/react-dom/src/events/DOMPluginEventSystem.js
export function attemptToDispatchEvent(
  domEventName: DOMEventName,
  eventSystemFlags: EventSystemFlags,
  targetContainer: EventTarget,
  nativeEvent: AnyNativeEvent,
): null | Container | SuspenseInstance {
  // 从原生事件对象中获取 target
  const nativeEventTarget = getEventTarget(nativeEvent);
  // // 获取 target 对应的 fiber 节点
  let targetInst = getClosestInstanceFromNode(nativeEventTarget);

  if (targetInst !== null) {
    // 一些兼容性处理...
  }
  // 通过插件系统,派发事件
  dispatchEventForPluginEventSystem(domEventName,eventSystemFlags, nativeEvent, targetInst, targetContainer);
  return null;
}
  1. dispatchEventsForPlugins

实际上 dispathEvent 中最核心的内容就是调用 dispatchEventsForPlugins,因为正是这个函数触发了 事件收集、事件执行

  • extractEvents中会进行事件合成
  • processDispatchQueue会根据事件阶段(冒泡或捕获)来决定是正序还是倒序遍历合成事件中的listeners
// packages/react-dom/src/events/DOMPluginEventSystem.js
function dispatchEventsForPlugins(
  domEventName: DOMEventName, // 事件名称
  eventSystemFlags: EventSystemFlags, // 事件处理阶段,4 = 捕获阶段,0 = 冒泡阶段
  nativeEvent: AnyNativeEvent, // 监听器的原生入参 Event 对象
  targetInst: null | Fiber, // event.target 对应的 DOM 节点的 Fiber 节点
  targetContainer: EventTarget, // 根 DOM 节点
): void {
  // 1、定位触发事件的原生DOM节点
  const nativeEventTarget = getEventTarget(nativeEvent);
  // 2、批量处理队列
  const dispatchQueue: DispatchQueue = [];
  // 3、收集 所有的listener
  extractEvents(
    dispatchQueue,
    domEventName,
    targetInst,
    nativeEvent,
    nativeEventTarget,
    eventSystemFlags,
    targetContainer,
  );
  // 4、执行派发
  processDispatchQueue(dispatchQueue, eventSystemFlags);
}

事件合成

在合成事件中,会根据domEventName来决定使用哪种类型的合成事件。

click为例,当我们点击页面的某个元素时,React会根据原生事件nativeEvent找到触发事件的DOM元素和对应的fiber节点。并以该节点为孩子节点往上查找,找到包括该节点及以上所有的click回调函数创建dispatchListener,并添加到listeners数组中

下面看下源码

  1. extractEvents

在extractEvents 函数中,主要调用了React的 5 种事件插件来收集listener。其中 SimpleEventPlugin 插件是React事件系统的核心,提供了React事件系统的基本功能。

// packages/react-dom/src/events/DOMPluginEventSystem.js#L95-L176
function extractEvents(
  dispatchQueue: DispatchQueue,
  domEventName: DOMEventName,
  targetInst: null | Fiber,
  nativeEvent: AnyNativeEvent,
  nativeEventTarget: null | EventTarget,
  eventSystemFlags: EventSystemFlags,
  targetContainer: EventTarget,
) {
  SimpleEventPlugin.extractEvents(
    dispatchQueue,
    domEventName,
    targetInst,
    nativeEvent,
    nativeEventTarget,
    eventSystemFlags,
    targetContainer,
  );
  const shouldProcessPolyfillPlugins =
    (eventSystemFlags & SHOULD_NOT_PROCESS_POLYFILL_EVENT_PLUGINS) === 0;
  if (shouldProcessPolyfillPlugins) {
    // 下面的几类插件本质上是 polyfills
    EnterLeaveEventPlugin.extractEvents(
      // 省略...
    );
    ChangeEventPlugin.extractEvents(
      // 省略...
    );
    SelectEventPlugin.extractEvents(
      // 省略...
    );
    BeforeInputEventPlugin.extractEvents(
      // 省略...
    );
  }
}
  1. SimpleEventPlugin
  • 首先初始化合成事件构造器(SyntheticEventCtor),SyntheticEvent是React内部的对象(构造函数),是原生事件的跨浏览器包装器,拥有和浏览器原生事件相同的接口(stopPropagationpreventDefault),抹平了不同浏览器的事件兼容性问题
  • 然后根据原生事件名称,初始化相应的合成事件构造函数
  • 接着分别在捕获阶段和冒泡阶段收集节点上所有监听该事件的 listener
  • 最后通过合成事件构造器构造合成事件,将合成事件添加到派发事件队列中
// packages/react-dom/src/events/DOMPluginEventSystem.js#L52-L216
function extractEvents(
  dispatchQueue: DispatchQueue,
  domEventName: DOMEventName,
  targetInst: null | Fiber,
  nativeEvent: AnyNativeEvent,
  nativeEventTarget: null | EventTarget,
  eventSystemFlags: EventSystemFlags,
  targetContainer: EventTarget,
): void {
  // 根据原生事件名称获取合成事件名称
  // 效果: onClick = topLevelEventsToReactNames.get('click')
  const reactName = topLevelEventsToReactNames.get(domEventName);
  if (reactName === undefined) {
    return;
  }
  // 默认合成函数的构造函数
  let SyntheticEventCtor = SyntheticEvent;
  let reactEventType: string = domEventName;
  // 按照原生事件名称来获取对应的合成事件构造函数 
  switch (domEventName) {
    // ...
    case 'keydown':
    case 'keyup':
      SyntheticEventCtor = SyntheticKeyboardEvent; // 键盘合成事件
      break;
    case 'focusin':
      reactEventType = 'focus';
      SyntheticEventCtor = SyntheticFocusEvent;   // 焦点合成事件
      break;
    // ...
  }
  // 是否是捕获阶段
  const inCapturePhase = (eventSystemFlags & IS_CAPTURE_PHASE) !== 0;
  if (
    enableCreateEventHandleAPI &&
    eventSystemFlags & IS_EVENT_HANDLE_NON_MANAGED_NODE
  ) {
    // 捕获阶段
    // 收集所有监听该事件的 listener
    const listeners = accumulateEventHandleNonManagedNodeListeners(
      ((reactEventType: any): DOMEventName),
      targetContainer,
      inCapturePhase,
    );
    if (listeners.length > 0) {
      // 构造合成事件, 添加到派发队列
      const event = new SyntheticEventCtor(
        reactName,
        reactEventType,
        null,
        nativeEvent,
        nativeEventTarget,
      );
      dispatchQueue.push({event, listeners});
    }
  } else {
    // scroll 事件不冒泡
    const accumulateTargetOnly =
      !inCapturePhase && domEventName === 'scroll';
    // 冒泡阶段
    // 核心,获取当前阶段的所有事件
    const listeners = accumulateSinglePhaseListeners(
      targetInst,
      reactName,
      nativeEvent.type,
      inCapturePhase,
      accumulateTargetOnly,
    );
    if (listeners.length > 0) {
      // 生成合成事件的 Event 对象
      const event = new SyntheticEventCtor(
        reactName,
        reactEventType,
        null,
        nativeEvent,
        nativeEventTarget,
      );
      // 入队
      dispatchQueue.push({event, listeners});
    }
  }
}
  1. SyntheticEventCtor

SyntheticEventCtor,合成事件根据事件类型有所不同都会调用createSyntheticEvent

React合成事件是将同类型的事件找出来,基于这个类型的事件,React通过代码定义好的类型事件的接口和原生事件创建相应的合成事件实例,并重写了preventDefaultstopPropagation方法。

这样,同类型的事件会复用同一个合成事件实例对象,节省了单独为每一个事件创建事件实例对象的开销,这就是事件的合成。

// packages/react-dom/src/events/SyntheticEvent.js
function createSyntheticEvent(Interface) {
  // 合成事件构造函数
  function SyntheticBaseEvent(reactName, reactEventType, targetInst, nativeEvent, nativeEventTarget) {
    this._reactName = reactName;
    this._targetInst = targetInst;
    this.type = reactEventType;
    this.nativeEvent = nativeEvent;
    this.target = nativeEventTarget;
    this.currentTarget = null;
    // React根据不同事件类型写了对应的属性接口,这里基于接口将原生事件上的属性clone到构造函数中
    for (var _propName in Interface) {... }

    const defaultPrevented = nativeEvent.defaultPrevented != null ? nativeEvent.defaultPrevented : nativeEvent.returnValue === false;
    if (defaultPrevented) {
      this.isDefaultPrevented = functionThatReturnsTrue;
    } else {
      this.isDefaultPrevented = functionThatReturnsFalse;
    }
    this.isPropagationStopped = functionThatReturnsFalse;
    return this;
  }

  Object.assign(SyntheticBaseEvent.prototype, {
    // 阻止默认事件
    preventDefault: function () {...},
    // 阻止捕获和冒泡阶段中当前事件的进一步传播
    stopPropagation: function () {...},
    // 合成事件不使用对象池了,这个事件是空的,没有意义,保存是为了向下兼容不报错。
    persist: function () {},
    isPersistent: functionThatReturnsTrue
  });
  return SyntheticBaseEvent;
}

4.accumulateSinglePhaseListeners accumulateSinglePhaseListeners 函数的作用是:从触发事件的DOM节点对应的fiber节点开始,向上遍历,直到根DOM容器(div#root),在这过程中,收集所有的listener,最后将收集的listener返回出去

// packages/react-dom/src/events/DOMPluginEventSystem.js#L712-L803
// 简化版本
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;

  while (instance !== null) {
    const {stateNode, tag} = instance;
    // 如果是有效节点则获取其事件
    if (tag === HostComponent && stateNode !== null) {
      lastHostComponent = stateNode;

      if (reactEventName !== null) {
        // 获取存储在 Fiber 节点上 Props 里的对应事件(如果存在)
        const listener = getListener(instance, reactEventName);
        if (listener != null) {
          // 入队
          listeners.push(
            // 简单返回一个 {instance, listener, lastHostComponent} 对象
            createDispatchListener(instance, listener, lastHostComponent),
          );
        }
      }
    }
    // scroll 不会冒泡,获取一次就结束了
    if (accumulateTargetOnly) {
      break;
    }
    // 其父级 Fiber 节点,向上递归
    instance = instance.return;
  }

  // 返回监听器集合
  return listeners;
}

捕获和冒泡

  1. processDispatchQueue 执行派发
export function processDispatchQueue(
  dispatchQueue: DispatchQueue,
  eventSystemFlags: EventSystemFlags,
): void {
  // 通过 eventSystemFlags 判断当前事件阶段
  const inCapturePhase = (eventSystemFlags & IS_CAPTURE_PHASE) !== 0;
  // 遍历合成事件
  for (let i = 0; i < dispatchQueue.length; i++) {
    // 取出其合成 Event 对象及事件集合
    const {event, listeners} = dispatchQueue[i];
    // 这个函数就负责事件的调用
    // 如果是捕获阶段的事件则倒序调用,反之为正序调用,调用时会传入合成 Event 对象
    processDispatchQueueItemsInOrder(event, listeners, inCapturePhase);
  }

  // 抛出中间产生的错误
  rethrowCaughtError();
}
  1. processDispatchQueueItemsInOrder
// packages/react-dom/src/events/DOMPluginEventSystem.js
function processDispatchQueueItemsInOrder(
  event: ReactSyntheticEvent,
  dispatchListeners: Array<DispatchListener>,
  inCapturePhase: boolean,
): void {
  let previousInstance;
  if (inCapturePhase) {
    // 1、捕获阶段 事件处理
    // 注意这里是倒叙遍历 listener
    for (let i = dispatchListeners.length - 1; i >= 0; i--) {
      const {instance, currentTarget, listener} = dispatchListeners[i];
      // event.isPropagationStopped()
      // stopPropagation() 方法被调用则会返回 true,没调用则会返回 false
      if (instance !== previousInstance && event.isPropagationStopped()) {
        return;
      }
      executeDispatch(event, listener, currentTarget);
      previousInstance = instance;
    }
  } else {
    // 2、冒泡阶段 事件处理
    // 注意这里是顺序遍历 listener
    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;
    }
  }
}

function executeDispatch(
  event: ReactSyntheticEvent, // onChange
  listener: Function, // 对应的回调函数
  currentTarget: EventTarget,
): void {
  // "onChange"
  const type = event.type || 'unknown-event'; 
  // 将当前dom元素赋值给合成事件的currentTarget
  event.currentTarget = currentTarget;
  // 执行回调函数,listener为回调函数, event为合成事件,最后执行listener(event)这个方法调用
  // 这样就回调到了我们在JSX中注册的callback。比如onClick={(event) => {console.log(1)}}
  // 现在就明白了callback怎么被调用的,以及event参数怎么传入callback里面的了
  invokeGuardedCallbackAndCatchFirstError(type, listener, undefined, event);
  event.currentTarget = null;
}

processDispatchQueueItemsInOrder 函数中遍历当前事件的所有listener,执行 executeDispatch 派发事件,在 fiber 节点上绑定的 listener 被执行。

在派发事件的过程中,根据捕获阶段和冒泡阶段的不同,对 dispatchListeners 采取了不同的遍历方式:

  • 捕获阶段从上往下 调用 fiber 树中绑定的回调函数,所以从后往前遍历 dispatchListeners
  • 冒泡阶段从下往上 调用 fiber 树中绑定的回调函数,所以从前往后遍历 dispatchListeners
  1. 流程图总结如下

image.png

参考文章