Click on the React source debug repository.

Why implement an event mechanism yourself

Due to the nature of the Fiber mechanism, when a fiber node is generated, its corresponding DOM node may not be mounted, and event handlers such as onClick, as a prop for the fiber node, cannot be directly bound to the real DOM node. React provides a “top-level registration, event collection, and unified triggering” event mechanism.

The “top-level registry” is essentially binding a unified event handler to the root element. “Event collection” refers to the construction of synthetic event objects that bubble or capture paths to collect the actual event handlers in the component when the event is triggered (actually the event handlers on root are executed). Unified triggering occurs after the collection process and executes one by one on the collected events, sharing the same composite event object. One important point here is that event listeners bound to root are not event handlers we write in the component. Note this distinction, which will be covered later.

The React event mechanism avoids the problem of binding events directly to DOM nodes, and makes good use of the Fiber tree hierarchy to generate event execution paths to simulate event capture and bubbling. It also provides two very important features:

  • Categorizing events can include different priorities for the tasks that the event generates
  • Provides composite event objects to smooth out browser compatibility differences

This article will explain the event mechanism in detail throughout the life cycle of an event from registration to execution.

Event registration

Unlike previous versions, React17’s events are registered with root rather than document. This is mainly for incremental upgrades to avoid event system collisions when multiple React versions coexist.

When we bind events to an element, we say:

<div onClick={() = > {/*do something*/}}>React</div>
Copy the code

The div node will eventually correspond to a Fiber node, with onClick acting as its prop. When the Fiber node enters the complete phase of the Render phase, a prop named onClick is recognized as an event for processing.

function setInitialDOMProperties(
  tag: string,
  domElement: Element,
  rootContainerElement: Element | Document,
  nextProps: Object,
  isCustomComponentTag: boolean,
) :void {
  for (const propKey in nextProps) {
    if(! nextProps.hasOwnProperty(propKey)) { ... }else if (registrationNameDependencies.hasOwnProperty(propKey)) {
        // If propKey is of event type, event binding is performedensureListeningTo(rootContainerElement, propKey, domElement); }}}}Copy the code

RegistrationNameDependencies is an object that stores the collection of all the React corresponds to the native DOM events, this is the basis of identify whether the prop for the event. If it is a prop of event type, ensureListeningTo will be called to bind the event.

The subsequent binding process can be summarized as the following key points:

  • For example, the onMouseEnter event relies on both mouseout and mouseover events. OnClick relies onClick only. For example, if the component is onClick, a click event listener is bound to root.
  • Identify which phase of the event (bubble or capture) it belongs to based on the event name written in the component, for exampleonClickCaptureThe React event name means that the event is required to fire during the capture phaseonClickRepresents events that need to be fired in the bubble phase.
  • Based on the React event name, find the corresponding native event name, for exampleclickAnd based on the previous step to determine whether to trigger, call during the capture phaseaddEventListenerBinds the event to the root element.
  • If the event needs to be updated, the event listener is removed and then rebound, repeating the previous three steps.

Through this process, the event listener is eventually bound to the root element.

  // Create event listeners with different priorities according to the event name.
  let listener = createEventListenerWrapperWithPriority(
    targetContainer,
    domEventName,
    eventSystemFlags,
    listenerPriority,
  );

  // Bind events
  if (isCapturePhaseListener) {
    ...
    unsubscribeListener = addEventCaptureListener(
      targetContainer,
      domEventName,
      listener,
    );
  } else{... unsubscribeListener = addEventBubbleListener( targetContainer, domEventName, listener, ); }Copy the code

Who is the event listener

When we bind events, the listener that is bound to root is a listener, but this listener is not an event handler that we write directly into the component. Through the above code, the listener is createEventListenerWrapperWithPriority call results

Why create a listener instead of binding the event handler directly to the component?

Actually createEventListenerWrapperWithPriority this function name already gave the answer: on the basis of priority to create an event monitor wrapper. There are two key points: priority and event listener wrappers. Priority refers to the event priority. For details about event priority, see Priority in React.

Event priorities are classified according to the interaction degree of events. The mapping between priorities and event names exists in a Map structure. CreateEventListenerWrapperWithPriority will according to the event name or incoming return different priority level of event listeners wrapper.

In general, there are three types of event listener wrappers:

  • DispatchDiscreteEvent: Processes discrete events
  • DispatchUserBlockingUpdate: handle user events
  • DispatchEvent: processes continuous events

These wrappers are event listeners that are actually bound to root and hold their respective priorities. When the corresponding event is fired, the event listener with the priority is actually called.

Transparent event execution phase flag

Root is tied to a priority event listener that triggers real events in the component. One thing that has not been included so far is the distinction between the execution stages of events. Registering an event in a component can distinguish the future execution phase with the event name + “Capture” suffix, but this is not the same thing as actually executing the event, so the key now is to implement the execution phase explicitly declared when registering the event into the action of executing the event.

At this point we can focus on one into the participation in the createEventListenerWrapperWithPriority function: eventSystemFlags. It is a flag for the event system that records various flags for the event, one of which is IS_CAPTURE_PHASE, which indicates that the current event is a capture phase trigger. When the event name contains the Capture suffix, eventSystemFlags is assigned to IS_CAPTURE_PHASE.

Later, when an event listener bound to root is created with priority, eventSystemFlags is passed in as an input parameter to its execution. As a result, events in the component are known to be executed in the order of bubbling or capturing when they fire.

function dispatchDiscreteEvent(domEventName, eventSystemFlags, container, nativeEvent,) {... discreteUpdates( dispatchEvent, domEventName, eventSystemFlags,// Pass the flag of the execution phase of the event
    container,
    nativeEvent,
  );
}
Copy the code

summary

Two things should be clear by now:

  1. Event handlers are not bound to component elements, but to root, due to the fiber tree structure, which means that event handlers can only be used as fiber prop.
  2. The event listener bound to root is not the event handler we wrote in the component, but a listener that holds the event priority and passes the event phase flag.

Now that the registration phase is complete, let’s look at how the event is triggered, starting with the listener bound to root and seeing what it does.

Event triggering – What does the event listener do

What it does can be summed up in one sentence: it is responsible for triggering the actual event flow with different priority weights and passing eventSystemFlags.

For example, if an element is bound to an onClick event, when clicked, the listener bound to root will be fired, eventually causing the event in the component to be executed.

This means that the event listener bound to root acts as a messenger, scheduling the next work according to the priority of the event: Composition of event objects, collection of event handlers to the execution path, and event execution so that the Scheduler can learn the priority of the current task later in the scheduling process and start scheduling.

How is priority passed on?

By calling the runWithPriority function in scheduler, the priority is recorded to the scheduler so that the scheduler knows the priority of the current task at schedule time. The second parameter, runWithPriority, schedules the three tasks mentioned above.

Take the user blocking priority level as an example:

function dispatchUserBlockingUpdate(domEventName, eventSystemFlags, container, nativeEvent,) {... runWithPriority( UserBlockingPriority, dispatchEvent.bind(null,
        domEventName,
        eventSystemFlags,
        container,
        nativeEvent,
      ),
    );
}

Copy the code

RunWithPriority dispatchUserBlockingUpdate calls and incoming UserBlockingPriority priority, so it can be UserBlockingPriority priority record to the Scheduler, React calculates various priorities based on this UserBlockingPriority priority.

In addition to passing priority, the other important things it does are trigger the synthesis of event objects, collect event handlers to the execution path, and execute the event to the execution phase of the event. Event listening on root eventually triggers dispatchEventsForPlugins.

This function body can be viewed as two parts: composition and collection of event objects, and event execution, covering the above three processes.

function dispatchEventsForPlugins(
  domEventName: DOMEventName,
  eventSystemFlags: EventSystemFlags,
  nativeEvent: AnyNativeEvent,
  targetInst: null | Fiber,
  targetContainer: EventTarget,
) :void {
  const nativeEventTarget = getEventTarget(nativeEvent);
  const dispatchQueue: DispatchQueue = [];

  // Event object composition, collect events to the execution path
  extractEvents(
    dispatchQueue,
    domEventName,
    targetInst,
    nativeEvent,
    nativeEventTarget,
    eventSystemFlags,
    targetContainer,
  );

  Execute the actual events in the collected components
  processDispatchQueue(dispatchQueue, eventSystemFlags);
}
Copy the code

The flow of events within the dispatchEventsForPlugins function has an important vehicle: the dispatchQueue, which carries the synthesized event object and the event handlers collected to the event execution path.

Listeners are event execution paths, events are synthetic event objects, real events in components are collected to the execution paths, and composition of event objects is implemented through extractEvents.

Composition of event objects and collection of events

It should be clear at this point that event listening on root triggers the synthesis of event objects and the collection of events in preparation for the actual event firing.

Composite event object

The event object in the event handler of the component is not a native event object, but a SyntheticEvent object synthesized by React. It addresses compatibility differences between browsers. Abstract into a unified event object to remove the mental burden of developers.

Event Execution path

When the event object is synthesized, the event is collected to the event execution path. What is the event execution path?

In the browser environment, if the parent element is bound to the same type of event, the events will be emitted in the order of bubbling or capturing unless manual intervention is performed.

React works the same way. Start with the element that triggers the event and look up in the fiber tree hierarchy to accumulate all the events of the same type in the upper element, and finally form an array of events of the same type. This array is the event execution path. With this path, React emulates its own event capture and bubbling mechanism.

Below is an overview of wrapping event objects and gathering events (bubbling paths, for example)

Because different events have different behaviors and processing mechanisms, the construction of synthesized event objects and the collection of events into the execution path need to be implemented through plug-ins. There are five types of Plugin: SimpleEventPlugin, EnterLeaveEventPlugin, ChangeEventPlugin, SelectEventPlugin, BeforeInputEventPlugin. They have exactly the same mission, just different categories of events, so there are some internal differences. This article uses only the SimpleEventPlugin as an example to illustrate this process, which handles more general event types such as Click, Input, KeyDown, and so on.

Below is the code in the SimpleEventPlugin to construct the composite event object and collect the events.

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 EventInterface;
  switch (domEventName) {
    // Assign EventInterface (interface)
  }

  // Construct the composite event object
  const event = new SyntheticEvent(
    reactName,
    null,
    nativeEvent,
    nativeEventTarget,
    EventInterface,
  );

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

  if (/ *... * /) {... }else {
    // Scroll event does not bubble
    constaccumulateTargetOnly = ! inCapturePhase && domEventName ==='scroll';

    // Event objects distribute & collect events
    accumulateSinglePhaseListeners(
      targetInst,
      dispatchQueue,
      event,
      inCapturePhase,
      accumulateTargetOnly,
    );
  }
  return event;
}
Copy the code

Create a composite event object

This unified event object is constructed from the SyntheticEvent function, which itself implements the browser’s event object interface in compliance with the W3C specification, so as to smooth out the differences, and the nativeEvent object is just one of its attributes (nativeEvent).

  // Construct the composite event object
  const event = new SyntheticEvent(
    reactName,
    null,
    nativeEvent,
    nativeEventTarget,
    EventInterface,
  );
Copy the code

Collect events to the execution path

The process is to collect the actual event handlers from the component into an array and wait for the next batch execution.

Let’s start with an example where the target element is counter and the parent element is counter-parent.

class EventDemo extends React.Component{
  state = { count: 0 }
  onDemoClick = () = > {
    console.log('Click on counter was triggered');
    this.setState({
      count: this.state.count + 1
    })
  }
  onParentClick = () = > {
    console.log('Parent element click event triggered');
  }
  render() {
    const { count } = this.state
    return <div
      className={'counter-parent'}
      onClick={this.onParentClick}
    >
      <div
        onClick={this.onDemoClick}
        className={'counter'}
      >
        {count}
      </div>
    </div>}}Copy the code

When you click on counter, the click event on the parent element is also triggered, printing one after the other:

'Click on counter is triggered' click on parent element is triggered 'Copy the code

This is actually caused by collecting events in a bubbling order into the execution path. The process of collecting performed by accumulateSinglePhaseListeners.

accumulateSinglePhaseListeners(
  targetInst,
  dispatchQueue,
  event,
  inCapturePhase,
  accumulateTargetOnly,
);
Copy the code

The most important operation inside the function is collecting the event to the execution path. To do this, you need to start in the Fiber tree from the source fiber node that triggered the event and work your way up to the root, forming a complete bubble or capture path. At the same time, when passing the Fiber node along the way, we get the event handler function that we wrote in the component from props according to the event name and push it to the path for the next batch execution.

The source code for this process is condensed below

export function accumulateSinglePhaseListeners(
  targetFiber: Fiber | null,
  dispatchQueue: DispatchQueue,
  event: ReactSyntheticEvent,
  inCapturePhase: boolean,
  accumulateTargetOnly: boolean,
) :void {

  // The event name identifies whether the event is in the bubble phase or in the capture phase
  const bubbled = event._reactName;
  constcaptured = bubbled ! = =null ? bubbled + 'Capture' : null;

  // Declare an array to hold event listeners
  const listeners: Array<DispatchListener> = [];

  // Find the target element
  let instance = targetFiber;

  // Add up all fiber objects and event listeners from the target element to root.
  while(instance ! = =null) {
    const {stateNode, tag} = instance;

    if(tag === HostComponent && stateNode ! = =null) {
      const currentTarget = stateNode;

      // Event capture
      if(captured ! = =null && inCapturePhase) {
        // Get the event handler from fiber
        const captureListener = getListener(instance, captured);
        if(captureListener ! =null) { listeners.push( createDispatchListener(instance, captureListener, currentTarget), ); }}// Events bubble up
      if(bubbled ! = =null && !inCapturePhase) {
        // Get the event handler from fiber
        const bubbleListener = getListener(instance, bubbled);
        if(bubbleListener ! =null) {
          listeners.push(
            createDispatchListener(instance, bubbleListener, currentTarget),
          );
        }
      }
    }
    instance = instance.return;
  }
  // Collect event objects
  if(listeners.length ! = =0) { dispatchQueue.push(createDispatchEntry(event, listeners)); }}Copy the code

Events are pushed to the dispatchQueue in the same order whether they are executed in the bubbling phase or the capture phase, but the order in which they are executed is due to the order in which the listeners array is emptied.

Note that each collection will only collect events of the same type as the event source, such as the child element bound to onClick and the parent element bound to onClick and onClickCapture:

<div
  className="parent"OnClick ={onClickParent} onClickCapture={onClickParentCapture} > Parent <div className="child"OnClick ={onClickChild} > </div> </div>Copy the code

When the child element is clicked, onClickChild and onClickParent will be collected.

The results of the collection are as follows

How does a synthetic event object participate in event execution

As we said above, the structure of a dispatchQueue looks like this

[
  {
    event: SyntheticEvent,
    listeners: [ listener1, listener2, ... ]
  }
]
Copy the code

An event represents a synthetic event object, which can be thought of as an event object shared by these listeners. When the clean listeners array is executed to each event listener, the event listener can change currentTarget on the event or call the stopPropagation method on it to prevent bubbling. Events, as a shared resource, are consumed by event listeners, and the consumption occurs when the event is executed.

Event to perform

Through the process of collecting events and event objects, a complete event execution path is obtained, and there is a shared event object. After entering the event execution process, the path is circulated from beginning to end, and the listener functions in each item are called in turn. This process focuses on the simulation of event bubbling and capture, and the application of synthesized event objects, as shown in the process of extracting event objects and time execution paths from the dispatchQueue.

export function processDispatchQueue(dispatchQueue: DispatchQueue, eventSystemFlags: EventSystemFlags,) :void {
  constinCapturePhase = (eventSystemFlags & IS_CAPTURE_PHASE) ! = =0;
  for (let i = 0; i < dispatchQueue.length; i++) {

    // Fetch the event object and the event listener array from dispatchQueue
    const {event, listeners} = dispatchQueue[i];

    / / will go to the trigger event listeners to a processDispatchQueueItemsInOrder, at the same time the incoming event object for the use of event listeners
    processDispatchQueueItemsInOrder(event, listeners, inCapturePhase);
  }
  // Catch an error
  rethrowCaughtError();
}
Copy the code

Simulate bubbling and capture

Bubbling and capturing are executed in a different order, but when collecting events, both bubbling and capturing were pushed directly into the path. So how does the difference in execution order manifest itself? The answer is that the order of the loop paths causes the execution order to be different.

Notice the sequence of event handlers in the dispatchQueue: the target element that triggered the event is first, and the listeners are second.

<div onClick={onClickParent}> Parent <div onClick={onClickChild}> Child </div> </div> listeners: [ onClickChild, onClickParent ]Copy the code

In a left-to-right loop, the target element’s events fire first, and the parent element’s events are executed sequentially, just like bubbling, so the capture sequence naturally loops from right to left. The code to simulate bubbling and capture execution events is as follows:

The execution phase of the event is determined according to inCapturePhase, whose source has been mentioned in the content of the transparent pass through event execution phase flag above.

function processDispatchQueueItemsInOrder(
  event: ReactSyntheticEvent,
  dispatchListeners: Array<DispatchListener>,
  inCapturePhase: boolean,
) :void {
  let previousInstance;

  if (inCapturePhase) {
    // Event capture reverse loop
    for (let i = dispatchListeners.length - 1; i >= 0; i--) {
      const {instance, currentTarget, listener} = dispatchListeners[i];
      if(instance ! == previousInstance && event.isPropagationStopped()) {return;
      }
      // Execute the event, passing in the Event object, and currentTargetexecuteDispatch(event, listener, currentTarget); previousInstance = instance; }}else {
    // Events bubble in a positive sequence loop
    for (let i = 0; i < dispatchListeners.length; i++) {
      const {instance, currentTarget, listener} = dispatchListeners[i];
      // If the event object prevents bubbling, return the loop
      if(instance ! == previousInstance && event.isPropagationStopped()) {return; } executeDispatch(event, listener, currentTarget); previousInstance = instance; }}}Copy the code

At this point, we write event handler is executed in the component, synthesis of the event object in the process serves as a public role, each event is executed, will check the synthetic event object, the way to stop the bubble ever call, will also present a mount event monitor elements as currentTarget mounted to the event object, Finally we pass in the event handler, and we get the event object.

conclusion

There is a lot of event system code in the source code, and I came out alive mainly with these questions to look at the code: what is the process of binding events, the relationship between the event system and priorities, and how the real event handler functions are executed.

To summarize the principles of the event mechanism: Because of the fiber tree, a component that contains a prop for an event will bind an event to the root during the Commit phase of the corresponding Fiber node. This event listener is prioritized, which ties it to the priority mechanism. Responsible for coordinating the synthesis of event objects, collecting events, and firing the actual event handlers.