React原理:通俗易懂的 合成事件

前言

React 17 开始,React 不再将事件处理添加到 document 处,而是将事件添加到渲染 React 树的根 DOM 节点上,下图是官方的示意图:

从上图中展示了这一变化,当然,无论是在document还是根 DOM 容器上监听事件, 都可以归为事件委托(代理)

请注意:并不是所有的事件都通过事件代理的方式来处理的,对于某些特殊事件,比如:scrollload,它们是通过listenToNonDelegatedEvent函数进行绑定.

上述特殊事件最大的不同是监听的 DOM 元素不同, 除此之外, 其他地方的实现与正常事件大体一致.本文讨论的是可以被根 DOM 容器代理的正常事件.

事件绑定

React 在启动时,会调用 createRootImpl 这个方法:

js 复制代码
function createRootImpl(
  container: Container,
  tag: RootTag,
  options: void | RootOptions,
) {
  // ... 省略无关代码
  if (enableEagerRootListeners) {
    const rootContainerElement =
    container.nodeType === COMMENT_NODE ? container.parentNode : container;
    listenToAllSupportedEvents(rootContainerElement);
  }
  // ... 省略无关代码
}

内部会调用 listenToAllSupportedEvents 函数, 实际上完成了事件代理

js 复制代码
// ... 省略无关代码
export function listenToAllSupportedEvents(rootContainerElement: EventTarget) {
  if (enableEagerRootListeners) {
    // 1. 节流优化, 保证全局注册只被调用一次
    if ((rootContainerElement: any)[listeningMarker]) {
      return;
    }
    (rootContainerElement: any)[listeningMarker] = true;
    // 2. 遍历allNativeEvents 监听冒泡和捕获阶段的事件
    allNativeEvents.forEach((domEventName) => {
      if (!nonDelegatedEvents.has(domEventName)) {
        listenToNativeEvent(
          domEventName,
          false, // 冒泡阶段监听
          ((rootContainerElement: any): Element),
          null,
        );
      }
      listenToNativeEvent(
        domEventName,
        true, // 捕获阶段监听
        ((rootContainerElement: any): Element),
        null,
      );
    });
  }
}

这部分的逻辑是:

  • 节流优化,保证事件全局中只注册一次
  • 遍历 allNativeEvents,内部调用listenToNativeEvent监听冒泡和捕获阶段的事件。allNativeEvents包括了大量的原生事件名称。说白了也就是在这里对原生事件的冒泡和捕获进行监听

现在,我们来看看 listenToNativeEvent 函数

js 复制代码
// ... 省略无关代码
export function listenToNativeEvent(
  domEventName: DOMEventName,
  isCapturePhaseListener: boolean,
  rootContainerElement: EventTarget,
  targetElement: Element | null,
  eventSystemFlags?: EventSystemFlags = 0,
): void {
  let target = rootContainerElement;


  const listenerSet = getEventListenerSet(target);
  const listenerSetKey = getListenerSetKey(
    domEventName,
    isCapturePhaseListener,
  );
  // 利用set数据结构, 保证相同的事件类型只会被注册一次.
  if (!listenerSet.has(listenerSetKey)) {
    if (isCapturePhaseListener) {
      eventSystemFlags |= IS_CAPTURE_PHASE;
    }
    // 注册事件监听
    addTrappedEventListener(
      target,
      domEventName,
      eventSystemFlags,
      isCapturePhaseListener,
    );
    listenerSet.add(listenerSetKey);
  }
}

它的内部去调用 addTrappedEventListener

js 复制代码
// ... 省略无关代码
function addTrappedEventListener(
  targetContainer: EventTarget,
  domEventName: DOMEventName,
  eventSystemFlags: EventSystemFlags,
  isCapturePhaseListener: boolean,
  isDeferredListenerForLegacyFBSupport?: boolean,
) {
  // 1. 构造listener,这是关键,它实现了把原生事件派发到 React 的体系内
  let listener = createEventListenerWrapperWithPriority(
    targetContainer,
    domEventName,
    eventSystemFlags,
  );
  let unsubscribeListener;
  // 2. 注册事件监听
  if (isCapturePhaseListener) {
    unsubscribeListener = addEventCaptureListener(
      targetContainer,
      domEventName,
      listener,
    );
  } else {
    unsubscribeListener = addEventBubbleListener(
      targetContainer,
      domEventName,
      listener,
    );
  }
}


// 注册原生事件 冒泡
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;
}

这部分调用链比较长,但最终会执行到 addEventBubbleListeneraddEventCaptureListener 来监听冒泡、捕获阶段的原生事件

然后,在这一过程中,listener 函数起着至关重要的作用,它实现了将原生事件派发到 React 体系内的功能。

比如点击 DOM 触发原生事件, 原生事件最后会被派发到react内部的onClick函数. listener函数就是这个由外至内的关键环节.

listener是通过createEventListenerWrapperWithPriority函数产生:

js 复制代码
export function createEventListenerWrapperWithPriority(
  targetContainer: EventTarget,
  domEventName: DOMEventName,
  eventSystemFlags: EventSystemFlags,
): Function {
  // 1. 根据优先级设置 listenerWrapper
  const eventPriority = getEventPriorityForPluginSystem(domEventName);
  let listenerWrapper;
  switch (eventPriority) {
    case DiscreteEvent:
      listenerWrapper = dispatchDiscreteEvent;
      break;
    case UserBlockingEvent:
      listenerWrapper = dispatchUserBlockingUpdate;
      break;
    case ContinuousEvent:
    default:
      listenerWrapper = dispatchEvent;
      break;
  }
  // 2. 返回 listenerWrapper
  return listenerWrapper.bind(
    null,
    domEventName,
    eventSystemFlags,
    targetContainer,
  );
}

可以看到, 不同的domEventName调用getEventPriorityForPluginSystem后返回不同的优先级, 最终会有 3 种情况:

  1. DiscreteEvent: 优先级最高, 包括click, keyDown, input等事件, 源码

  2. UserBlockingEvent: 优先级适中, 包括drag, scroll等事件, 源码

  3. ContinuousEvent: 优先级最低,包括animation, load等事件, 源码

这 3 种listener实际上都是对dispatchEvent的包装:

js 复制代码
// ...省略无关代码
export function dispatchEvent(
  domEventName: DOMEventName,
  eventSystemFlags: EventSystemFlags,
  targetContainer: EventTarget,
  nativeEvent: AnyNativeEvent,
): void {
  if (!_enabled) {
    return;
  }
  const blockedOn = attemptToDispatchEvent(
    domEventName,
    eventSystemFlags,
    targetContainer,
    nativeEvent,
  );
}

事件触发

当原生事件触发之后, 首先会进入到dispatchEvent这个回调函数. 而dispatchEvent函数是react事件体系中最关键的函数, 其调用链路较长, 核心步骤如图所示:

重点关注其中 3 个核心环节:

  1. attemptToDispatchEvent
  2. SimpleEventPlugin.extractEvents
  3. processDispatchQueue

attemptToDispatchEvent

attemptToDispatchEvent 把原生事件和fiber树关联起来

js 复制代码
export function attemptToDispatchEvent(
  domEventName: DOMEventName,
  eventSystemFlags: EventSystemFlags,
  targetContainer: EventTarget,
  nativeEvent: AnyNativeEvent,
): null | Container | SuspenseInstance {
  // ...省略无关代码


  // 1. 定位原生DOM节点
  const nativeEventTarget = getEventTarget(nativeEvent);
  // 2. 获取与DOM节点对应的fiber节点
  let targetInst = getClosestInstanceFromNode(nativeEventTarget);
  // 3. 通过插件系统, 派发事件
  dispatchEventForPluginEventSystem(
    domEventName,
    eventSystemFlags,
    nativeEvent,
    targetInst,
    targetContainer,
  );
  return null;
}

它的逻辑:

  1. 定位原生 DOM 节点: 调用getEventTarget
  2. 获取与 DOM 节点对应的 fiber 节点: 调用getClosestInstanceFromNode
  3. 通过插件系统, 派发事件: 调用 dispatchEventForPluginEventSystem

extractEvents

dispatchEvent函数的调用链路中, 通过不同的插件, 处理不同的事件. 其中最常见的事件都会由SimpleEventPlugin.extractEvents进行处理

js 复制代码
function extractEvents(
  dispatchQueue: DispatchQueue,
  domEventName: DOMEventName,
  targetInst: null | Fiber,
  nativeEvent: AnyNativeEvent,
  nativeEventTarget: null | EventTarget,
  eventSystemFlags: EventSystemFlags,
  targetContainer: EventTarget,
): void {
  const reactName = topLevelEventsToReactNames.get(domEventName);
  if (reactName === undefined) {
    return;
  }
  let SyntheticEventCtor = SyntheticEvent;
  let reactEventType: string = domEventName;


  const inCapturePhase = (eventSystemFlags & IS_CAPTURE_PHASE) !== 0;
  const accumulateTargetOnly = !inCapturePhase && domEventName === 'scroll';
  // 1. 收集所有监听该事件的函数.
  const listeners = accumulateSinglePhaseListeners(
    targetInst,
    reactName,
    nativeEvent.type,
    inCapturePhase,
    accumulateTargetOnly,
  );
  if (listeners.length > 0) {
    // 2. 构造合成事件, 添加到派发队列
    const event = new SyntheticEventCtor(
      reactName,
      reactEventType,
      null,
      nativeEvent,
      nativeEventTarget,
    );
    dispatchQueue.push({ event, listeners });
  }
}

它的核心:

  • 收集所有监听该事件的函数. 调用 accumulateSinglePhaseListeners 函数
  • 构造合成事件(SyntheticEventSyntheticEvent, 是react内部创建的一个对象, 是原生事件的跨浏览器包装器, 拥有和浏览器原生事件相同的接口(stopPropagation,preventDefault), 抹平不同浏览器 api 的差异, 兼容性好.)
  • 添加到派发队列(dispatchQueue)

processDispatchQueue

processDispatchQueue 执行真正的事件派发

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);
  }
  // ...省略无关代码
}


function processDispatchQueueItemsInOrder(
  event: ReactSyntheticEvent,
  dispatchListeners: Array<DispatchListener>,
  inCapturePhase: boolean,
): void {
  let previousInstance;
  if (inCapturePhase) {
    // 1. capture事件: 倒序遍历listeners
    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 {
    // 2. bubble事件: 顺序遍历listeners
    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 去遍历 dispatchListeners 数组,通过 executeDispatch 函数派发事件,在fiber节点上绑定的listener函数被执行
  • 根据捕获(capture)冒泡(bubble)的不同, 采取了不同的遍历方式:
    1. capture事件: 从上至下调用fiber树中绑定的回调函数, 所以倒序遍历dispatchListeners.
    2. bubble事件: 从下至上调用fiber树中绑定的回调函数, 所以顺序遍历dispatchListeners.

至此,事件的整个流程完成

总结

SyntheticEvent打通了从外部原生事件到内部fiber树的交互渠道, 使得react能够感知到浏览器提供的原生事件, 进而做出不同的响应, 修改fiber树, 变更视图等.

主要分为 3 步:

  1. 监听原生事件: 找到DOM元素对应的fiber元素
  2. 收集listeners: 遍历fiber树, 收集所有监听本事件的listener函数.
  3. 派发合成事件: 构造合成事件, 遍历listeners进行派发.
相关推荐
Rattenking1 小时前
React 源码学习01 ---- React.Children.map 的实现与应用
javascript·学习·react.js
熊的猫2 小时前
JS 中的类型 & 类型判断 & 类型转换
前端·javascript·vue.js·chrome·react.js·前端框架·node.js
小牛itbull6 小时前
ReactPress:重塑内容管理的未来
react.js·github·reactpress
FinGet18 小时前
那总结下来,react就是落后了
前端·react.js
王解21 小时前
Jest项目实战(2): 项目开发与测试
前端·javascript·react.js·arcgis·typescript·单元测试
AIoT科技物语2 天前
免费,基于React + ECharts 国产开源 IoT 物联网 Web 可视化数据大屏
前端·物联网·react.js·开源·echarts
初遇你时动了情2 天前
react 18 react-router-dom V6 路由传参的几种方式
react.js·typescript·react-router
番茄小酱0012 天前
ReactNative中实现图片保存到手机相册
react native·react.js·智能手机
王解2 天前
Jest进阶知识:深入测试 React Hooks-确保自定义逻辑的可靠性
前端·javascript·react.js·typescript·单元测试·前端框架
小牛itbull2 天前
ReactPress—基于React的免费开源博客&CMS内容管理系统
前端·react.js·开源·reactpress