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进行派发.
相关推荐
screct_demo5 小时前
詳細講一下在RN(ReactNative)中,6個比較常用的組件以及詳細的用法
javascript·react native·react.js
光头程序员14 小时前
grid 布局react组件可以循数据自定义渲染某个数据 ,或插入某些数据在某个索引下
javascript·react.js·ecmascript
limit for me14 小时前
react上增加错误边界 当存在错误时 不会显示白屏
前端·react.js·前端框架
浏览器爱好者14 小时前
如何构建一个简单的React应用?
前端·react.js·前端框架
VillanelleS17 小时前
React进阶之高阶组件HOC、react hooks、自定义hooks
前端·react.js·前端框架
傻小胖18 小时前
React 中hooks之useInsertionEffect用法总结
前端·javascript·react.js
flying robot1 天前
React的响应式
前端·javascript·react.js
GISer_Jing2 天前
React+AntDesign实现类似Chatgpt交互界面
前端·javascript·react.js·前端框架
智界工具库2 天前
【探索前端技术之 React Three.js—— 简单的人脸动捕与 3D 模型表情同步应用】
前端·javascript·react.js
我是前端小学生2 天前
我们应该在什么场景下使用 useMemo 和 useCallback ?
react.js