synthetic-event

h7ml
  • react
大约 8 分钟

React 合成事件

概览

v17.0.0开始, React 不会再将事件处理添加到 document 上, 而是将事件处理添加到渲染 React 树的根 DOM 容器中.

引入官方提供的图片:

图中清晰的展示了v17.0.0的改动, 无论是在document还是根 DOM 容器上监听事件, 都可以归为事件委托(代理)(mdnopen in new window).

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

上述特殊事件最大的不同是监听的 DOM 元素不同, 除此之外, 其他地方的实现与正常事件大体一致.

本节讨论的是可以被根 DOM 容器代理的正常事件.

事件绑定

在前文 React 应用的启动过程中介绍了React在启动时会创建全局对象, 其中在创建 fiberRoot 对象时, 调用createRootImplopen in new window:

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

listenToAllSupportedEventsopen in new window函数, 实际上完成了事件代理:

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

核心逻辑:

  1. 节流优化, 保证全局注册只被调用一次.
  2. 遍历allNativeEvents, 调用listenToNativeEvent监听冒泡和捕获阶段的事件.

listenToNativeEventopen in new window:

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

addTrappedEventListeneropen in new window:

// ... 省略无关代码
function addTrappedEventListener(
  targetContainer: EventTarget,
  domEventName: DOMEventName,
  eventSystemFlags: EventSystemFlags,
  isCapturePhaseListener: boolean,
  isDeferredListenerForLegacyFBSupport?: boolean
) {
  // 1. 构造listener
  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;
}

listenToAllSupportedEvents开始, 调用链路比较长, 最后调用addEventBubbleListeneraddEventCaptureListener监听了原生事件.

原生 listener

在注册原生事件的过程中, 需要重点关注一下监听函数, 即listener函数. 它实现了把原生事件派发到react体系之内, 非常关键.

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

listener是通过createEventListenerWrapperWithPriority函数产生:

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等事件, 源码open in new window
  2. UserBlockingEvent: 优先级适中, 包括drag, scroll等事件, 源码open in new window
  3. ContinuousEvent: 优先级最低,包括animation, load等事件, 源码open in new window

这 3 种listener实际上都是对dispatchEventopen in new window的包装:

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

关联 fiber

attemptToDispatchEventopen in new window把原生事件和fiber树关联起来.

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

收集 fiber 上的 listener

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

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

核心逻辑:

  1. 收集所有listener回调

    • 这里的是fiber.memoizedProps.onClick/onClickCapture等绑定在fiber节点上的回调函数

    • 具体逻辑在accumulateSinglePhaseListenersopen in new window:

      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;
      
        // 从targetFiber开始, 向上遍历, 直到 root 为止
        while (instance !== null) {
          const { stateNode, tag } = instance;
          // 当节点类型是HostComponent时(如: div, span, button等类型)
          if (tag === HostComponent && stateNode !== null) {
            lastHostComponent = stateNode;
            if (reactEventName !== null) {
              // 获取标准的监听函数 (如onClick , onClickCapture等)
              const listener = getListener(instance, reactEventName);
              if (listener != null) {
                listeners.push(createDispatchListener(instance, listener, lastHostComponent));
              }
            }
          }
          // 如果只收集目标节点, 则不用向上遍历, 直接退出
          if (accumulateTargetOnly) {
            break;
          }
          instance = instance.return;
        }
        return listeners;
      }
      
  2. 构造合成事件(SyntheticEvent), 添加到派发队列(dispatchQueue)

构造合成事件

SyntheticEventopen in new window, 是react内部创建的一个对象, 是原生事件的跨浏览器包装器, 拥有和浏览器原生事件相同的接口(stopPropagation,preventDefault), 抹平不同浏览器 api 的差异, 兼容性好.

具体的构造过程并不复杂, 可以直接查看源码open in new window.

此处我们需要知道, 在Plugin.extractEvents过程中, 遍历fiber树找到listener之后, 就会创建SyntheticEvent, 加入到dispatchQueue中, 等待派发.

执行派发

extractEvents完成之后, 逻辑来到processDispatchQueueopen in new window, 终于要真正执行派发了.

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

processDispatchQueueItemsInOrderopen in new window遍历dispatchListeners数组, 执行executeDispatchopen in new window派发事件, 在fiber节点上绑定的listener函数被执行.

processDispatchQueueItemsInOrder函数中, 根据捕获(capture)冒泡(bubble)的不同, 采取了不同的遍历方式:

  1. capture事件: 从上至下调用fiber树中绑定的回调函数, 所以倒序遍历dispatchListeners.
  2. bubble事件: 从下至上调用fiber树中绑定的回调函数, 所以顺序遍历dispatchListeners.

总结

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

从实现上讲, 主要分为 3 步:

  1. 监听原生事件: 对齐DOM元素fiber元素
  2. 收集listeners: 遍历fiber树, 收集所有监听本事件的listener函数.
  3. 派发合成事件: 构造合成事件, 遍历listeners进行派发.