React 合成事件

概览

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

引入官方提供的图片:

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

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

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

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

事件绑定

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

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

listenToAllSupportedEvents函数, 实际上完成了事件代理:

1// ... 省略无关代码
2export function listenToAllSupportedEvents(rootContainerElement: EventTarget) {
3  if (enableEagerRootListeners) {
4    // 1. 节流优化, 保证全局注册只被调用一次
5    if ((rootContainerElement: any)[listeningMarker]) {
6      return;
7    }
8    (rootContainerElement: any)[listeningMarker] = true;
9    // 2. 遍历allNativeEvents 监听冒泡和捕获阶段的事件
10    allNativeEvents.forEach((domEventName) => {
11      if (!nonDelegatedEvents.has(domEventName)) {
12        listenToNativeEvent(
13          domEventName,
14          false, // 冒泡阶段监听
15          ((rootContainerElement: any): Element),
16          null
17        );
18      }
19      listenToNativeEvent(
20        domEventName,
21        true, // 捕获阶段监听
22        ((rootContainerElement: any): Element),
23        null
24      );
25    });
26  }
27}

核心逻辑:

  1. 节流优化, 保证全局注册只被调用一次.
  2. 遍历allNativeEvents, 调用listenToNativeEvent监听冒泡和捕获阶段的事件.
    • allNativeEvents包括了大量的原生事件名称, 它是在DOMPluginEventSystem.js被初始化

listenToNativeEvent:

1// ... 省略无关代码
2export function listenToNativeEvent(
3  domEventName: DOMEventName,
4  isCapturePhaseListener: boolean,
5  rootContainerElement: EventTarget,
6  targetElement: Element | null,
7  eventSystemFlags?: EventSystemFlags = 0
8): void {
9  let target = rootContainerElement;
10
11  const listenerSet = getEventListenerSet(target);
12  const listenerSetKey = getListenerSetKey(domEventName, isCapturePhaseListener);
13  // 利用set数据结构, 保证相同的事件类型只会被注册一次.
14  if (!listenerSet.has(listenerSetKey)) {
15    if (isCapturePhaseListener) {
16      eventSystemFlags |= IS_CAPTURE_PHASE;
17    }
18    // 注册事件监听
19    addTrappedEventListener(target, domEventName, eventSystemFlags, isCapturePhaseListener);
20    listenerSet.add(listenerSetKey);
21  }
22}

addTrappedEventListener:

1// ... 省略无关代码
2function addTrappedEventListener(
3  targetContainer: EventTarget,
4  domEventName: DOMEventName,
5  eventSystemFlags: EventSystemFlags,
6  isCapturePhaseListener: boolean,
7  isDeferredListenerForLegacyFBSupport?: boolean
8) {
9  // 1. 构造listener
10  let listener = createEventListenerWrapperWithPriority(targetContainer, domEventName, eventSystemFlags);
11  let unsubscribeListener;
12  // 2. 注册事件监听
13  if (isCapturePhaseListener) {
14    unsubscribeListener = addEventCaptureListener(targetContainer, domEventName, listener);
15  } else {
16    unsubscribeListener = addEventBubbleListener(targetContainer, domEventName, listener);
17  }
18}
19
20// 注册原生事件 冒泡
21export function addEventBubbleListener(target: EventTarget, eventType: string, listener: Function): Function {
22  target.addEventListener(eventType, listener, false);
23  return listener;
24}
25
26// 注册原生事件 捕获
27export function addEventCaptureListener(target: EventTarget, eventType: string, listener: Function): Function {
28  target.addEventListener(eventType, listener, true);
29  return listener;
30}

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

原生 listener

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

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

listener是通过createEventListenerWrapperWithPriority函数产生:

1export function createEventListenerWrapperWithPriority(
2  targetContainer: EventTarget,
3  domEventName: DOMEventName,
4  eventSystemFlags: EventSystemFlags
5): Function {
6  // 1. 根据优先级设置 listenerWrapper
7  const eventPriority = getEventPriorityForPluginSystem(domEventName);
8  let listenerWrapper;
9  switch (eventPriority) {
10    case DiscreteEvent:
11      listenerWrapper = dispatchDiscreteEvent;
12      break;
13    case UserBlockingEvent:
14      listenerWrapper = dispatchUserBlockingUpdate;
15      break;
16    case ContinuousEvent:
17    default:
18      listenerWrapper = dispatchEvent;
19      break;
20  }
21  // 2. 返回 listenerWrapper
22  return listenerWrapper.bind(null, domEventName, eventSystemFlags, targetContainer);
23}

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

  1. DiscreteEvent: 优先级最高, 包括click, keyDown, input等事件, 源码
  2. UserBlockingEvent: 优先级适中, 包括drag, scroll等事件, 源码
  3. ContinuousEvent: 优先级最低,包括animation, load等事件, 源码

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

1// ...省略无关代码
2export function dispatchEvent(
3  domEventName: DOMEventName,
4  eventSystemFlags: EventSystemFlags,
5  targetContainer: EventTarget,
6  nativeEvent: AnyNativeEvent
7): void {
8  if (!_enabled) {
9    return;
10  }
11  const blockedOn = attemptToDispatchEvent(domEventName, eventSystemFlags, targetContainer, nativeEvent);
12}

事件触发

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

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

  1. attemptToDispatchEvent
  2. SimpleEventPlugin.extractEvents
  3. processDispatchQueue

关联 fiber

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

1export function attemptToDispatchEvent(
2  domEventName: DOMEventName,
3  eventSystemFlags: EventSystemFlags,
4  targetContainer: EventTarget,
5  nativeEvent: AnyNativeEvent
6): null | Container | SuspenseInstance {
7  // ...省略无关代码
8
9  // 1. 定位原生DOM节点
10  const nativeEventTarget = getEventTarget(nativeEvent);
11  // 2. 获取与DOM节点对应的fiber节点
12  let targetInst = getClosestInstanceFromNode(nativeEventTarget);
13  // 3. 通过插件系统, 派发事件
14  dispatchEventForPluginEventSystem(domEventName, eventSystemFlags, nativeEvent, targetInst, targetContainer);
15  return null;
16}

核心逻辑:

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

收集 fiber 上的 listener

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

1function extractEvents(
2  dispatchQueue: DispatchQueue,
3  domEventName: DOMEventName,
4  targetInst: null | Fiber,
5  nativeEvent: AnyNativeEvent,
6  nativeEventTarget: null | EventTarget,
7  eventSystemFlags: EventSystemFlags,
8  targetContainer: EventTarget
9): void {
10  const reactName = topLevelEventsToReactNames.get(domEventName);
11  if (reactName === undefined) {
12    return;
13  }
14  let SyntheticEventCtor = SyntheticEvent;
15  let reactEventType: string = domEventName;
16
17  const inCapturePhase = (eventSystemFlags & IS_CAPTURE_PHASE) !== 0;
18  const accumulateTargetOnly = !inCapturePhase && domEventName === 'scroll';
19  // 1. 收集所有监听该事件的函数.
20  const listeners = accumulateSinglePhaseListeners(
21    targetInst,
22    reactName,
23    nativeEvent.type,
24    inCapturePhase,
25    accumulateTargetOnly
26  );
27  if (listeners.length > 0) {
28    // 2. 构造合成事件, 添加到派发队列
29    const event = new SyntheticEventCtor(reactName, reactEventType, null, nativeEvent, nativeEventTarget);
30    dispatchQueue.push({ event, listeners });
31  }
32}

核心逻辑:

  1. 收集所有listener回调

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

    • 具体逻辑在accumulateSinglePhaseListeners:

      1export function accumulateSinglePhaseListeners(
      2  targetFiber: Fiber | null,
      3  reactName: string | null,
      4  nativeEventType: string,
      5  inCapturePhase: boolean,
      6  accumulateTargetOnly: boolean
      7): Array<DispatchListener> {
      8  const captureName = reactName !== null ? reactName + 'Capture' : null;
      9  const reactEventName = inCapturePhase ? captureName : reactName;
      10  const listeners: Array<DispatchListener> = [];
      11
      12  let instance = targetFiber;
      13  let lastHostComponent = null;
      14
      15  // 从targetFiber开始, 向上遍历, 直到 root 为止
      16  while (instance !== null) {
      17    const { stateNode, tag } = instance;
      18    // 当节点类型是HostComponent时(如: div, span, button等类型)
      19    if (tag === HostComponent && stateNode !== null) {
      20      lastHostComponent = stateNode;
      21      if (reactEventName !== null) {
      22        // 获取标准的监听函数 (如onClick , onClickCapture等)
      23        const listener = getListener(instance, reactEventName);
      24        if (listener != null) {
      25          listeners.push(createDispatchListener(instance, listener, lastHostComponent));
      26        }
      27      }
      28    }
      29    // 如果只收集目标节点, 则不用向上遍历, 直接退出
      30    if (accumulateTargetOnly) {
      31      break;
      32    }
      33    instance = instance.return;
      34  }
      35  return listeners;
      36}
  2. 构造合成事件(SyntheticEvent), 添加到派发队列(dispatchQueue)

构造合成事件

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

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

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

执行派发

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

1export function processDispatchQueue(dispatchQueue: DispatchQueue, eventSystemFlags: EventSystemFlags): void {
2  const inCapturePhase = (eventSystemFlags & IS_CAPTURE_PHASE) !== 0;
3  for (let i = 0; i < dispatchQueue.length; i++) {
4    const { event, listeners } = dispatchQueue[i];
5    processDispatchQueueItemsInOrder(event, listeners, inCapturePhase);
6  }
7  // ...省略无关代码
8}
9
10function processDispatchQueueItemsInOrder(
11  event: ReactSyntheticEvent,
12  dispatchListeners: Array<DispatchListener>,
13  inCapturePhase: boolean
14): void {
15  let previousInstance;
16  if (inCapturePhase) {
17    // 1. capture事件: 倒序遍历listeners
18    for (let i = dispatchListeners.length - 1; i >= 0; i--) {
19      const { instance, currentTarget, listener } = dispatchListeners[i];
20      if (instance !== previousInstance && event.isPropagationStopped()) {
21        return;
22      }
23      executeDispatch(event, listener, currentTarget);
24      previousInstance = instance;
25    }
26  } else {
27    // 2. bubble事件: 顺序遍历listeners
28    for (let i = 0; i < dispatchListeners.length; i++) {
29      const { instance, currentTarget, listener } = dispatchListeners[i];
30      if (instance !== previousInstance && event.isPropagationStopped()) {
31        return;
32      }
33      executeDispatch(event, listener, currentTarget);
34      previousInstance = instance;
35    }
36  }
37}

processDispatchQueueItemsInOrder遍历dispatchListeners数组, 执行executeDispatch派发事件, 在fiber节点上绑定的listener函数被执行.

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

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

总结

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

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

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