React 合成事件源码解析

1,852 阅读10分钟

前置阅读资料

阅读下面几篇文章有助于理解今天的内容。但是不阅读也没有关系 ~

  1. 事件处理
  2. 什么是 SyntheticEvent
  3. React 17 关于事件委托的变更

正文

在浏览器的世界里,我们通常使用 addEventListener 去绑定事件,对于一个被触发的事件,按照时间的顺序,会先后经历三个阶段:

  • 捕获阶段(Capture Phase)
  • 目标阶段(Target Phase)
  • 冒泡阶段(Bubbing Phase)

image.png

相应的,addEventListener 也有相应的入参可以指定我们在哪一个阶段去执行回调函数:

target.addEventListener(type, listener, true); // 监听捕获阶段
target.addEventListener(type, listener); // 监听冒泡阶段

React 的事件系统中也实现了相应的功能,拿点击事件来说,默认的 onClick 事件是监听冒泡阶段的,而我们可以使用 onClickCapture 去监听捕获阶段:

<button
    className="btn"
    onClick={handleClick}
    onClickCaputre={handleClickCapture}
>
    Hello
</button>

由于两个阶段的触发时机不同,导致了触发的顺序也不同,在 React 内部,也是把两个阶段分开进行处理。

接下来,我们即将分析它的源码,但是在分析之前,我们先看一下上面这一小段 JSX 代码被编译后的模样:

React.createElement("button", {
  className: "btn",
  onClick: handleClick,
  onClickCaputre: handleClickCapture
}, "Hello");

可以看到,onClick 并没有什么特别的,它和 className 一样,就只是作为 props 的一个值被传入,在后面,也和 className 一样,再把它的值存储在此元素对应的 Fiber 节点上。等到用到的时候就从 Fiber 节点上读取。 那它是怎么做到在点击的时候被执行的呢?别着急,看完这篇文章你就理解了。

React 的整个事件系统嵌套的函数还是比较多的,为了避免大家看晕,可以先大概看一下它的流程:

// 0. 在根节点上进行事件委托
app.addEventListener('click', (e) => {
  // 当事件被触发时:
  // 1. 找到 e.target 属性找到当前点击的 DOM 元素
  // 2. 根据当前的 DOM 元素找到对应的 Fiber 节点
  // 3. 收集 Fiber 节点到根节点这一链路上的 onClick 函数
  // 4. 派发收集到的这些函数
})

下面我们正式进入源码解读的部分。

当我们调用 React.render 或者 React.createRoot 初始化应用的时候,我们首先会创建应用的根级 Fiber 节点,随后就会调用 listenToAllSupportedEvents 来绑定事件:

// rootContainerElement 是我们应用根节点所对应的 DOM 元素
listenToAllSupportedEvents(rootContainerElement);

listenToAllSupportedEvents 会遍历 allNativeEvents 这个 Set 对象。allNativeEvents 里面含有一些原生事件的名称,如:'click'、'input'、'focus'。会在刚开始加载 React 源码的时候就被注入进去:

// 这些方法的作用之一就是为 allNativeEvents 注入了一些事件名
SimpleEventPlugin.registerEvents();
EnterLeaveEventPlugin.registerEvents();
ChangeEventPlugin.registerEvents();
SelectEventPlugin.registerEvents();
BeforeInputEventPlugin.registerEvents();

接下来看 listenToAllSupportedEvents 做了什么。

export function listenToAllSupportedEvents(
    rootContainerElement: EventTarget
) {
  // 保证事件只注册一次,在下次调用此函数的时候就不注册了
  if (!(rootContainerElement: any)[listeningMarker]) {
    (rootContainerElement: any)[listeningMarker] = true;
    // 这是一个 set 结构,里面的值有各种原生事件的名称
    allNativeEvents.forEach(domEventName => {
      if (domEventName !== 'selectionchange') {
        // 有些特殊的事件不需要委托到应用的根节点,
        // 如 'cancel'、'load'、'scroll' 等
        if (!nonDelegatedEvents.has(domEventName)) {
          // 在根节点上绑定对此事件的冒泡阶段的事件委托
          listenToNativeEvent(domEventName, false, rootContainerElement);
        }
        //  在根节点上绑定对此事件的捕获阶段的事件委托
        listenToNativeEvent(domEventName, true, rootContainerElement);
      }
    });
  }
}

接着,我们看来 listenToNativeEvent 做了什么。

export function listenToNativeEvent(
  // 原生事件名
  domEventName: DOMEventName, 
  // true 的话,就是捕获阶段,反之,冒泡阶段
  isCapturePhaseListener: boolean, 
  // 添加事件委托的节点,目前来说,就是根节点
  target: EventTarget,
): void {
  let eventSystemFlags = 0;
  
  // 是否标记为捕获阶段
  if (isCapturePhaseListener) {
    eventSystemFlags |= IS_CAPTURE_PHASE;
  }
  
  addTrappedEventListener(
    target,
    domEventName,
    eventSystemFlags,
    isCapturePhaseListener,
  );
}

这一段代码是比较直观的,它在最后调用了 addTrappedEventListener,这个函数的作用就是为我们的根节点绑定事件。

function addTrappedEventListener(
  targetContainer: EventTarget,
  domEventName: DOMEventName,
  eventSystemFlags: EventSystemFlags,
  isCapturePhaseListener: boolean,
  isDeferredListenerForLegacyFBSupport?: boolean,
) {
  
  // 创建事件委托的回调函数
  let listener = createEventListenerWrapperWithPriority(
    targetContainer,
    domEventName,
    eventSystemFlags,
  );

  let unsubscribeListener;

  // 根据捕获阶段还是冒泡阶段,会调用两个不同的函数
  if (isCapturePhaseListener) {
      unsubscribeListener = addEventCaptureListener(
        targetContainer,
        domEventName,
        listener,
      );
  } else {
    unsubscribeListener = addEventBubbleListener(
      targetContainer,
      domEventName,
      listener,
    );
  }
}

addEventCaptureListeneraddEventBubbleListener 非常的相像,就只是传给 addEventListener 的最后的参数不一样,这不禁让我想问,为什么不再开放一个参数,写成一个函数呢?

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

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

为了不影响主流程,在上面的 addTrappedEventListener中, 我省略了对 isPassiveListener 判断的讲解,它们的大意就是:如果浏览器的 addEventListener 支持 passive 这个属性,且当前绑定事件名为 'touchstart'、'touchmove'、'wheel' 中的一种,就默认为这些属性的绑定事件增加 passive,这能很大的提升性能。如果你想了解 passive 的作用,可参考这个回答

如果你想看 addTrappedEventListenr 的全部内容, 点我查看

到这里,算是告一段落了,我们已经为根节点绑定好了事件。

接下来,当在页面进行活动,事件被触发了,就会调用回调函数。调用的回调函数就在上面的代码中,叫做 listener,它是由createEventListenerWrapperWithPriority 生成的。这个函数的代码很直观:

export function createEventListenerWrapperWithPriority(
  targetContainer: EventTarget,
  domEventName: DOMEventName,
  eventSystemFlags: EventSystemFlags,
): Function {
  // 根据事件名获取优先级,一般的事件如 'click'、'input' 都是 
  // DiscreteEventPriority 级别的
  const eventPriority = getEventPriority(domEventName);
  let listenerWrapper;
  switch (eventPriority) {
    case DiscreteEventPriority:
      listenerWrapper = dispatchDiscreteEvent;
      break;
    case ContinuousEventPriority:
      listenerWrapper = dispatchContinuousEvent;
      break;
    case DefaultEventPriority:
    default:
      listenerWrapper = dispatchEvent;
      break;
  }
  
  // 这个返回值就是我们的回调函数 listener 
  // listenerWrapper 其实接受四个参数,我们当前只绑定了前三个,
  // 第四个就是在触发事件的时候,调用回调传入的,
  // 也就是 DOM 的 Event 对象。
  return listenerWrapper.bind(
    null,
    domEventName,
    eventSystemFlags,
    targetContainer,
  );
}

我们以 dispatchDiscreteEvent 为例看后面的流程。事实上,我们发现,它也好,dispatchContinuousEvent 也好,最终都还是调用了 dispatchEvent

function dispatchDiscreteEvent(
  domEventName,
  eventSystemFlags,
  container,
  nativeEvent,
) {
  const previousPriority = getCurrentUpdatePriority();
  const prevTransition = ReactCurrentBatchConfig.transition;
  ReactCurrentBatchConfig.transition = 0;
  
  try {
    setCurrentUpdatePriority(DiscreteEventPriority);
    dispatchEvent(domEventName, eventSystemFlags, container, nativeEvent);
  } finally {
    setCurrentUpdatePriority(previousPriority);
    ReactCurrentBatchConfig.transition = prevTransition;
  }
}

所以,我们来研究 dispatchEvent 做了什么,为了不影响主流程的阅读,我也省略了其他代码,剩下的,就是它调用了 attemptToDispatchEvent 方法。

export function dispatchEvent(
  domEventName: DOMEventName,
  eventSystemFlags: EventSystemFlags,
  targetContainer: EventTarget,
  nativeEvent: AnyNativeEvent,
): void {
  let blockedOn = attemptToDispatchEvent(
    domEventName,
    eventSystemFlags,
    targetContainer,
    nativeEvent,
  );
}

attemptToDispatchEvent 方法就是尝试去派发一个事件,成功的时候,会返回 null,不成功会返回 SuspenseInstance 或者 Container(当我写下的时候,我也不知道这个是什么,不过这并不影响我们阅读后面的流程)。

export function attemptToDispatchEvent(
  domEventName: DOMEventName,
  eventSystemFlags: EventSystemFlags,
  targetContainer: EventTarget,
  nativeEvent: AnyNativeEvent,
): null | Container | SuspenseInstance {

  // 获取当前点击的 DOM 节点
  // 一般情况下,就是读取 nativeEvent.target
  const nativeEventTarget = getEventTarget(nativeEvent);

  // 获取 DOM 节点对应的 Fiber 实例
  let targetInst = getClosestInstanceFromNode(nativeEventTarget);

  // 得到的 Fiber 实例可能有一些问题,不是想要的,这里做兼容
  // 只是知道有这段逻辑就好,不想看可以直接忽略,
  if (targetInst !== null) {
    const nearestMounted = getNearestMountedFiber(targetInst);
    if (nearestMounted === null) {
      // This tree has been unmounted already. Dispatch without a target.
      targetInst = null;
    } else {
      const tag = nearestMounted.tag;
      if (tag === SuspenseComponent) {
        const instance = getSuspenseInstanceFromFiber(nearestMounted);
        if (instance !== null) {
          // Queue the event to be replayed later. Abort dispatching since we
          // don't want this event dispatched twice through the event system.
          // TODO: If this is the first discrete event in the queue. Schedule an increased
          // priority for this boundary.
          return instance;
        }
        // This shouldn't happen, something went wrong but to avoid blocking
        // the whole system, dispatch the event without a target.
        // TODO: Warn.
        targetInst = null;
      } else if (tag === HostRoot) {
        const root: FiberRoot = nearestMounted.stateNode;
        if (root.isDehydrated) {
          // If this happens during a replay something went wrong and it might block
          // the whole system.
          return getContainerFromFiber(nearestMounted);
        }
        targetInst = null;
      } else if (nearestMounted !== targetInst) {
        // If we get an event (ex: img onload) before committing that
        // component's mount, ignore it for now (that is, treat it as if it was an
        // event on a non-React tree). We might also consider queueing events and
        // dispatching them after the mount.
        targetInst = null;
      }
    }
  }

  // 收集 Fiber 节点上的事件,并开始派发
  dispatchEventForPluginEventSystem(
    domEventName,
    eventSystemFlags,
    nativeEvent,
    targetInst,
    targetContainer,
  );
  
  // 正常情况会走到这里,返回 null
  return null;
}

再来看上面函数在返回前最后调用的 dispatchEventForPluginEventSystem

export function dispatchEventForPluginEventSystem(
  domEventName: DOMEventName,
  eventSystemFlags: EventSystemFlags,
  nativeEvent: AnyNativeEvent,
  targetInst: null | Fiber,
  targetContainer: EventTarget,
): void {
  
  batchedUpdates(() =>
    dispatchEventsForPlugins(
      domEventName,
      eventSystemFlags,
      nativeEvent,
      ancestorInst,
      targetContainer,
    ),
  );
}

dispatchEventsForPlugins 的逻辑还是比较清晰的:

function dispatchEventsForPlugins(
  domEventName: DOMEventName,
  eventSystemFlags: EventSystemFlags,
  nativeEvent: AnyNativeEvent,
  targetInst: null | Fiber,
  targetContainer: EventTarget,
): void {
  
  // 拿到当前点击的 DOM 节点
  const nativeEventTarget = getEventTarget(nativeEvent);
  const dispatchQueue: DispatchQueue = [];
  
  // 从当前 Fiber 节点出发,收集整个链路上的事件
  extractEvents(
    dispatchQueue,
    domEventName,
    targetInst,
    nativeEvent,
    nativeEventTarget,
    eventSystemFlags,
    targetContainer,
  );
  
  // 依次触发收集到的事件
  processDispatchQueue(dispatchQueue, eventSystemFlags);
}

剩下的内容,就主要是 extractEventsprocessDispatchQueue 这两个函数了。

先来看 extractEvents。它会调用 SimpleEventPlugin.extractEvents

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

你可能知道,React 事件回调函数的参数不是原生的,而是经过它特殊处理的,叫做 SyntheticEvent,更多的信息,可以前往 官网这一节 阅读。生成 SyntheticEvent 的过程就是在下面这段代码中。

接下来是 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;

  // 根据事件名找到构造器
  switch (domEventName) {
    // ... 省略了其他判断
    case 'click':
      // Firefox creates a click event on right mouse clicks. This removes the
      // unwanted click events.
      if (nativeEvent.button === 2) {
        return;
      }
    default:
      break;
  }

  const inCapturePhase = (eventSystemFlags & IS_CAPTURE_PHASE) !== 0;

  // 从当前 Fiber 节点出发,根据事件名
  // 从当前节点的 props 中收集监听事件,收集完当前节点
  // 就去收集它的父节点,一直到顶。
  const listeners = accumulateSinglePhaseListeners(
    targetInst,
    reactName,
    nativeEvent.type,
    inCapturePhase,
    accumulateTargetOnly,
    nativeEvent,
  );

  if (listeners.length > 0) {
    // 创建基本的 EventTarget
    const event = new SyntheticEventCtor(
      reactName,
      reactEventType,
      null,
      nativeEvent,
      nativeEventTarget,
    );

    // 加入到派发队列中
    dispatchQueue.push({ event, listeners });
  }
}

只要理解了如何遍历 Fiber 树,accumulateSinglePhaseListeners 的代码就比较容易理解了,在这里我就忽略了,有想看的同学 点击这里 直达。

当调用完 extractEvents,我们的 dispatchQueue 就收集好了所有的回调函数,接下来就需要调用 processDispatchQueue 去依次调用这些函数。这也是我们最后看的一个函数了。

export function processDispatchQueue(
  dispatchQueue: DispatchQueue, // 我们前面收集的事件数组
  eventSystemFlags: EventSystemFlags, // 包含了是否是捕获阶段的信息
): void {
  // true 的话是捕获阶段,这会影响后续事件的执行顺序
  const inCapturePhase = (eventSystemFlags & IS_CAPTURE_PHASE) !== 0;
  
  for (let i = 0; i < dispatchQueue.length; i++) {
    const {event, listeners} = dispatchQueue[i];
    processDispatchQueueItemsInOrder(event, listeners, inCapturePhase);
   
  }
}

有一点值得注意,dispatchListeners 包含了所有需要执行的回调函数,它的顺序是从当前元素一直添加到根节点,也就是根节点是最后一个。既然是这样,那捕获阶段和冒泡阶段执行的顺序就会不一样,基于这个原因,再来看 processDispatchQueueItemsInOrder

function processDispatchQueueItemsInOrder(
  event: ReactSyntheticEvent,
  dispatchListeners: Array<DispatchListener>,
  inCapturePhase: boolean,
): void {
  let previousInstance;
  
  if (inCapturePhase) {
    // 捕获阶段,先执行最后面的事件
    for (let i = dispatchListeners.length - 1; i >= 0; i--) {
      const {instance, currentTarget, listener} = dispatchListeners[i];
      
      // 判断当前是否已经停止冒泡了,是的话,不执行后面的,直接 return
      if (instance !== previousInstance && event.isPropagationStopped()) {
        return;
      }
      
      executeDispatch(event, listener, currentTarget);
      previousInstance = instance;
    }
  } else {
    // 冒泡阶段,先执行前面的
    for (let i = 0; i < dispatchListeners.length; i++) {
      const {instance, currentTarget, listener} = dispatchListeners[i];
      
      // 判断当前是否已经停止冒泡了,是的话,不执行后面的,直接 return
      if (instance !== previousInstance && event.isPropagationStopped()) {
        return;
      }
      executeDispatch(event, listener, currentTarget);
      previousInstance = instance;
    }
  }
}

为什么 event.isPropagationStopped 能起到阻止冒泡的作用呢?

因为 React 的事件对象重写了 stopPropagation 方法。此时,所有的 event 都来自于一个父类。只不过,当在事件处理过程中有一个函数执行了 e.stopPropagation,就会修改父类的 isPropagationStopped 方法,此时后面的函数都会返回 true

stopPropagation: function() {
  const event = this.nativeEvent;
  if (!event) {
    return;
  }

  if (event.stopPropagation) {
    // 这里也是导致此现象的原因,就是因为在此阻止冒泡了
    // https://reactjs.org/blog/2020/08/10/react-v17-rc.html#fixing-potential-issues
    event.stopPropagation();
  } 

  this.isPropagationStopped = functionThatReturnsTrue;
}

// ...省略其他部分...

function functionThatReturnsTrue() {
  return true;
}

到这里,React 的合成事件的整个流程就分析完了,虽然代码量有点多,但是只要细心一点,理解整个流程还是不算难的。

看完 React 的合成事件机制,为我之前的无知感到懊悔,我之前还以为 React 不是用事件委托做的,并且我还很鄙视自己公司框架的事件模块。

希望本文能帮助你理解它,也非常感谢你阅读。