In the browser world, we usually use addEventListener to bind events. For an event to be triggered, there are three phases, in chronological order:

  • Capture Phase
  • Target Phase
  • Bubbing Phase

Accordingly, addEventListener has an input parameter that specifies at which stage the callback will be executed:

target.addEventListener(type, listener, true); // Listen for the capture phase
target.addEventListener(type, listener); // Listen for the bubble phase
Copy the code

The React event system also implements this function. For example, the default onClick event is to listen for the bubble phase. We can use onClickCapture to listen for the capture phase:

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

The trigger timing of the two phases is different, resulting in different trigger sequences. React processes the two phases separately.

Next, we’ll look at the source code, but before we do, let’s take a look at the compiled JSX code above:

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

As you can see, onClick is nothing special. Just like className, it is passed as a value for props, and later, just like className, its value is stored on the corresponding Fiber node of this element. So how does it get executed when you click on it? Don’t worry, you’ll understand after reading this article.

The React event system has a lot of nested functions. To avoid confusion, take a look at the process:

// 0. Delegate events on the root nodeapp? .addEventListener('click'.(e) = > {
  // 1. Find the e.target attribute to find the currently clicked DOM element
  // 2. Find the corresponding Fiber node based on the current DOM element
  Collect the onClick function on the link from the Fiber node to the root node
  // 4. Distribute the collected functions
})
Copy the code

Now we officially enter the source code interpretation part.

When we call the React. Render or React. CreateRoot initializes the application, we will first create the application’s root level Fiber node, then is called listenToAllSupportedEvents to bind an event:

// rootContainerElement is the DOM element corresponding to the root node of our application
listenToAllSupportedEvents(rootContainerElement);
Copy the code

ListenToAllSupportedEvents traverses allNativeEvents this Set object. AllNativeEvents contains the names of native events such as ‘click’, ‘input’, and ‘focus’. The React source will be injected as soon as it is loaded:

// One of the things these methods do is inject some event names into allNativeEvents
SimpleEventPlugin.registerEvents();
EnterLeaveEventPlugin.registerEvents();
ChangeEventPlugin.registerEvents();
SelectEventPlugin.registerEvents();
BeforeInputEventPlugin.registerEvents();
Copy the code

What see listenToAllSupportedEvents do next.

export function listenToAllSupportedEvents(
    rootContainerElement: EventTarget
) {
  // Ensure that the event is registered only once and will not be registered the next time this function is called
  if(! (rootContainerElement: any)[listeningMarker]) { (rootContainerElement: any)[listeningMarker] =true;
    // This is a set structure where values have the names of various native events
    allNativeEvents.forEach(domEventName= > {
      if(domEventName ! = ='selectionchange') {
        // Some special events do not need to be delegated to the root node of the application.
        // Such as 'cancel', 'load', 'scroll', etc
        if(! nonDelegatedEvents.has(domEventName)) {// Bind the event delegate for the bubbling phase of this event on the root node
          listenToNativeEvent(domEventName, false, rootContainerElement);
        }
        Bind the event delegate for the capture phase of this event on the root node
        listenToNativeEvent(domEventName, true, rootContainerElement); }}); }}Copy the code

And then we look at what the listenToNativeEvent does.

export function listenToNativeEvent(
  // Native event name
  domEventName: DOMEventName, 
  // True is the capture phase, and vice versa is the bubble phase
  isCapturePhaseListener: boolean, 
  // The node to which the event delegate is added is, for now, the root node
  target: EventTarget,
) :void {
  let eventSystemFlags = 0;
  
  // Whether to mark the capture phase
  if (isCapturePhaseListener) {
    eventSystemFlags |= IS_CAPTURE_PHASE;
  }
  
  addTrappedEventListener(
    target,
    domEventName,
    eventSystemFlags,
    isCapturePhaseListener,
  );
}
Copy the code

This piece of code is fairly straightforward. It ends with a call to addTrappedEventListener, which binds events to our root node.

function addTrappedEventListener(targetContainer: EventTarget, domEventName: DOMEventName, eventSystemFlags: EventSystemFlags, isCapturePhaseListener: boolean, isDeferredListenerForLegacyFBSupport? : boolean,) {
  
  // Create the event delegate callback function
  let listener = createEventListenerWrapperWithPriority(
    targetContainer,
    domEventName,
    eventSystemFlags,
  );

  let unsubscribeListener;

  // Two different functions are called depending on whether the capture phase or the bubble phase
  if (isCapturePhaseListener) {
      unsubscribeListener = addEventCaptureListener(
        targetContainer,
        domEventName,
        listener,
      );
  } else{ unsubscribeListener = addEventBubbleListener( targetContainer, domEventName, listener, ); }}Copy the code

The addEventCaptureListener is very similar to the addEventBubbleListener, except that the last parameter passed to the addEventListener is different, which makes me wonder, why not open a parameter and write it as a function?

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;
}
Copy the code

In order not to affect the main flow, I have omitted the explanation of isPassiveListener judgments in the addTrappedEventListener above, which basically says: If the browser addEventListener supports passive, and the current binding event is named ‘touchStart’, ‘touchMove’, or ‘wheel’, then by default passive is added to the binding event. This can greatly improve performance. If you want to understand the role of passive, please refer to this answer.

If you want to see all of addTrappedEventListenr, click me to view it.

At this point, we’re done. We’ve bound events to the root node.

Next, when an activity is performed on the page and the event is triggered, the callback function is called. Invoke the callback function in the code above, is called the listener, it is generated by createEventListenerWrapperWithPriority. The code for this function is straightforward:

export function createEventListenerWrapperWithPriority(targetContainer: EventTarget, domEventName: DOMEventName, eventSystemFlags: EventSystemFlags,) :Function {
  // Get priority by event name, common events such as 'click' and '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;
  }
  
  // The return value is our callback function listener
  // listenerWrapper actually accepts four arguments and we are currently only binding the first three,
  // The fourth is passed in by the callback when the event is raised.
  // The Event object for the DOM.
  return listenerWrapper.bind(
    null,
    domEventName,
    eventSystemFlags,
    targetContainer,
  );
}
Copy the code

Let’s take dispatchDiscreteEvent as an example for the following flow. In fact, we found that both it and dispatchContinuousEvent end up calling 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; }}Copy the code

AttemptToDispatchEvent so, to investigate what dispatchEvent does, I also omit the other code in order not to affect the reading of the main process. All that remains is that it calls the attemptToDispatchEvent method.

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

The attemptToDispatchEvent method attemptToDispatchEvent is an attempt to dispatch an event, returning null on success and SuspenseInstance or Container on failure (I don’t know what this is, However, this does not affect the flow we read later).

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

  // Get the DOM node currently clicked
  // NativeEvent.target is normally read
  const nativeEventTarget = getEventTarget(nativeEvent);

  // Get the Fiber instance corresponding to the DOM node
  let targetInst = getClosestInstanceFromNode(nativeEventTarget);

  // The obtained Fiber instance may have some problems, not desired, here do compatibility
  // If you don't want to see this, you can ignore it.
  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; }}}// Collect events on the Fiber node and start dispatching
  dispatchEventForPluginEventSystem(
    domEventName,
    eventSystemFlags,
    nativeEvent,
    targetInst,
    targetContainer,
  );
  
  // This is where you would normally go and return null
  return null;
}
Copy the code

Look at the above function dispatchEventForPluginEventSystem before returning the last call:

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

Here, we see that dispatchEventsForPlugins are wrapped by batchedUpdates. I will update a separate blog to explain the batch updates. After all, it’s kind of a waste of time to write it here, because I don’t know if anyone saw it. For now, let’s just ignore it.

The logic of dispatchEventsForPlugins is fairly clear:

function dispatchEventsForPlugins(
  domEventName: DOMEventName,
  eventSystemFlags: EventSystemFlags,
  nativeEvent: AnyNativeEvent,
  targetInst: null | Fiber,
  targetContainer: EventTarget,
) :void {
  
  // Get the currently clicked DOM node
  const nativeEventTarget = getEventTarget(nativeEvent);
  const dispatchQueue: DispatchQueue = [];
  
  // Collect events on the entire link from the current Fiber node
  extractEvents(
    dispatchQueue,
    domEventName,
    targetInst,
    nativeEvent,
    nativeEventTarget,
    eventSystemFlags,
    targetContainer,
  );
  
  // Triggers the collected events in turn
  processDispatchQueue(dispatchQueue, eventSystemFlags);
}
Copy the code

The remaining functions are mainly extractEvents and processDispatchQueue.

Let’s start with extractEvents. It calls the 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,
  );
}
Copy the code

As you may know, the React event callback argument is not native. Instead, it has a special handler called a SyntheticEvent. For more information, visit the section on the React event website. The process of generating a SyntheticEvent is in the following code.

Next is the SimpleEventPlugin. ExtractEvents code:

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;

  // Find the constructor by the event name
  switch (domEventName) {
    / /... Other judgments are omitted
    case 'click':
      // Firefox creates a click event on right mouse clicks. This removes the
      // unwanted click events.
      if (nativeEvent.button === 2) {
        return;
      }
    default:
      break;
  }

  constinCapturePhase = (eventSystemFlags & IS_CAPTURE_PHASE) ! = =0;

  // Start from the current Fiber node, according to the event name
  // Collect listen events from props of the current node
  // collect its parent node all the way to the top.
  const listeners = accumulateSinglePhaseListeners(
    targetInst,
    reactName,
    nativeEvent.type,
    inCapturePhase,
    accumulateTargetOnly,
    nativeEvent,
  );

  if (listeners.length > 0) {
    // Create a basic EventTarget
    const event = new SyntheticEventCtor(
      reactName,
      reactEventType,
      null,
      nativeEvent,
      nativeEventTarget,
    );

    // Join the dispatch queuedispatchQueue.push({ event, listeners }); }}Copy the code

As long as understanding how to traverse the tree Fiber, accumulateSinglePhaseListeners code is easy to understand, I ignored here, have a classmate want to see Click here to direct.

When the extractEvents are called, our dispatchQueue has collected all the callback functions and needs to call processDispatchQueue to call them in turn. And this is the last function we’re going to look at.

export function processDispatchQueue(
  dispatchQueue: DispatchQueue, // The event array we collected earlier
  eventSystemFlags: EventSystemFlags, // Contains information about whether it is the capture phase
) :void {
  // True is the capture phase, which affects the execution order of subsequent events
  constinCapturePhase = (eventSystemFlags & IS_CAPTURE_PHASE) ! = =0;
  
  for (let i = 0; i < dispatchQueue.length; i++) {
    const{event, listeners} = dispatchQueue[i]; processDispatchQueueItemsInOrder(event, listeners, inCapturePhase); }}Copy the code

Note that dispatchListeners contain all callbacks that need to be executed from the current element to the root node, where the root node is the last. Since it is, the capture phase and bubble phase sequence would be different, for this reason, look at processDispatchQueueItemsInOrder:

function processDispatchQueueItemsInOrder(
  event: ReactSyntheticEvent,
  dispatchListeners: Array<DispatchListener>,
  inCapturePhase: boolean,
) :void {
  let previousInstance;
  
  if (inCapturePhase) {
    // The last event is executed first in the capture phase
    for (let i = dispatchListeners.length - 1; i >= 0; i--) {
      const {instance, currentTarget, listener} = dispatchListeners[i];
      
      // Check whether the bubble has stopped, if so, do not perform the following, directly return
      if(instance ! == previousInstance && event.isPropagationStopped()) {return; } executeDispatch(event, listener, currentTarget); previousInstance = instance; }}else {
    // The bubble phase is executed first
    for (let i = 0; i < dispatchListeners.length; i++) {
      const {instance, currentTarget, listener} = dispatchListeners[i];
      
      // Check whether the bubble has stopped, if so, do not perform the following, directly return
      if(instance ! == previousInstance && event.isPropagationStopped()) {return; } executeDispatch(event, listener, currentTarget); previousInstance = instance; }}}Copy the code

Why the event. IsPropagationStopped can have the effect of prevent bubble?

The React event object overrides the stopPropagation method. At this point, all events come from a parent class. However, when a function executes e.topPropagation during event processing, the parent isPropagationStopped method is modified, and all subsequent functions return true.

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

  if (event.stopPropagation) {
    event.stopPropagation();
  } 

  this.isPropagationStopped = functionThatReturnsTrue;
}

/ /... Leave out the rest...

function functionThatReturnsTrue() {
  return true;
}
Copy the code

React synthesizes events in a way that is easy to understand if you are careful.

After watching React’s synthetic event mechanism, I felt sorry for my ignorance before. I thought React was not done by event delegation before, and I despised the event module of my company’s framework.

Hope this article helped you understand it, and thank you very much for reading it.