This article is based on React 17.0.0 and analyzes the React-DOM in Legacy Mode

The introduction

React’s composite event system has always been one of React’s signature features. The effect is to add an intermediate layer between developers and real events, allowing developers to output apis that match their design intentions, and native events to be hijacked and manipulated. The virtual DOM adopts a similar layered design, with a layer of abstraction between the developer and the native DOM. This abstract design method is worth learning in depth, and synthesizing the event system is an indispensable step to understand the React principle, hence this article. If there are any mistakes, please correct them.

consistency

In order to facilitate understanding and prevent disagreement, it is necessary to highlight and explain some concepts that may be confusing in this article. Please note that definitions are only responsible for this article and do not necessarily apply to content outside of this article.

Noun form

Nouns name paraphrase
Real events An event written in a component by a developer, attempted to mount on the DOM, or understood as a real event.
Native events That exists and happens in the browserUI Events, such as ‘click’, ‘cancel’, etc.

Composite event object Encapsulated by ReactThe Event object.
Synthetic event system React refers to the entire system that handles the event mechanism.
Root DOM node Not the Document node, but the DOM node mounted by the React application.
The event agent It is also called event delegation, which is the same as event delegation. I think the term “agency” is more suitable for use in this article, so I hereby state:)

The purpose of synthesizing event systems

Since there are no official answers, I can only offer some subjective ones: Performance optimization: using event brokers to uniformly receive native event firing, so that events are not bound on the real DOM. (Conversely, too many meaningless event collections can be triggered.) Layered design: Solve cross-platform problems. Compose event objects: Smooth out browser differences. React is used to handle native events first and then React is passed to real events. React can know which native events are triggered, which native events invoke the corresponding real events, and which events are triggered in the real events related to React state changes. One reason why the current React synthetic event system is irreplaceable is that React needs to know what native event triggered the update. React hostage event trigger lets you know what event was triggered by the user and what real event was invoked through the native event. In this way, the priority of the real event can be determined by defining the priority of the native event, which can then determine the priority of the update triggered within the real event, and finally determine when the corresponding update should be updated.

The initial combination is the event system

The initialization part of the work is relatively simple, the core purpose is to generate some static variables for later use. I will briefly cover important static variables and will not go into their initialization because the source code is very convoluted for such trivial matters. Entrance to the program in the react – dom/SRC/events/DOMPluginEventSystem js# L89 – L93, interested can debug. The five EventPlugin relationships can be understood as SimpleEventPlugin being the basic functional implementation of the composite event system, while the other eventplugins are merely polyfills of it.

SimpleEventPlugin.registerEvents();
EnterLeaveEventPlugin.registerEvents();
ChangeEventPlugin.registerEvents();
SelectEventPlugin.registerEvents();
BeforeInputEventPlugin.registerEvents();
Copy the code

eventPriorities

Mapping of native events and their priorities

{
  "cancel": 0.// ...
  "drag": 1.// ...
  "abort": 2.// ...
}
Copy the code

React defines priorities for native events in three main categories

export const DiscreteEvent: EventPriority = 0; // Discrete events, such as cancel, click, and mousedown, are of the lowest priority
export const UserBlockingEvent: EventPriority = 1; // User blocking events, such as Drag, mousemove, wheel, etc
export const ContinuousEvent: EventPriority = 2; // Continuous events, load, error, waiting and other media-related events require timely response, so they have the highest priority
Copy the code

topLevelEventsToReactNames

Mapping of native and synthesized events

{
  "cancel": "onCancel".// ...
  "pointercancel": "onPointerCancel".// ...
  "waiting": "onWaiting"
}
Copy the code

registrationNameDependencies

A mapping of synthesized events to the set of native events on which they depend

{
  "onCancel": ["cancel"]."onCancelCapture": ["cancel"].// ...
  "onChange": ["change"."click"."focusin"."focusout"."input"."keydown"."keyup"."selectionchange"]."onCancelCapture": ["change"."click"."focusin"."focusout"."input"."keydown"."keyup"."selectionchange"]."onSelect": ["focusout"."contextmenu"."dragend"."focusin"."keydown"."keyup"."mousedown"."mouseup"."selectionchange"]."onSelectCapture": ["focusout"."contextmenu"."dragend"."focusin"."keydown"."keyup"."mousedown"."mouseup"."selectionchange"].// ...
}
Copy the code

other

In addition, some variables or constants that are not all dynamically generated are also briefly introduced:

variable The data type meaning
allNativeEvents Set A collection of all meaningful native event names
nonDelegatedEvents Set A collection of native event names that do not need to be brokered (delegated) during the bubbling phase

Register event broker

17. X has changed greatly compared with 16. X, please pay attention to screening.


React uses the event broker to capture native events that occur in the browser, and then uses the native eventsEventObject to collect real events and then call real events.

We’ll talk about collecting events later, but let’s focus on the first half.”The event agent“At what stage and how it was done.



In version 17.x, createReactRootPhase is calledlistenToAllSupportedEventsDelta function and delta functionAll native events that can be listened onAdd listening events to.



The call chain is as follows:



listenToAllSupportedEvents

export function listenToAllSupportedEvents(rootContainerElement: EventTarget) {
  // The rootContainerElement element is passed in by the function that creates ReactRoot. Its content is the root DOM node of the React application.
  
  // enableEagerRootListeners are constant identifiers, often true, and can be ignored.
  // This means that the "add listeners on all native events as early as possible" feature is enabled, as opposed to the 16.x version where listeners are added later on demand.
  if (enableEagerRootListeners) {
    
    // listeningMarker is a marker consisting of fixed and random characters. It is used to identify whether the node has added listening events on all native events in react mode.
    // If it has already been added, skip it to save some unnecessary work
    if ((rootContainerElement: any)[listeningMarker]) {
      return;
    }
    
    // Add the identity
    (rootContainerElement: any)[listeningMarker] = true;
    
    // Iterate over all native events
    // In addition to native events that do not need to add event agents during the bubble phase, only the capture phase should add event agents
    // The rest of the events need to be added in the capture, bubble phase
    allNativeEvents.forEach(domEventName= > {
      if(! nonDelegatedEvents.has(domEventName)) { listenToNativeEvent( domEventName,false,
          ((rootContainerElement: any): Element),
          null,); } listenToNativeEvent( domEventName,true,
        ((rootContainerElement: any): Element),
        null,); }); }}Copy the code

listenToNativeEvent

// Simplified version
export function listenToNativeEvent(
  domEventName: DOMEventName,
  isCapturePhaseListener: boolean,
  rootContainerElement: EventTarget,
  targetElement: Element | null, eventSystemFlags? : EventSystemFlags =0.) :void {
  let target = rootContainerElement;
  
  // ...
  
  // The target node stores a value of type Set, which stores the native event names of listeners added to prevent repeated listener addition.
  const listenerSet = getEventListenerSet(target);
  
  / / effect: 'cancel' - > 'cancel__capture' | 'cancel__bubble'
  // Get the name of the event to be placed in the listenerSet
  const listenerSetKey = getListenerSetKey(domEventName, isCapturePhaseListener);
  
  // If not, bind
  if(! listenerSet.has(listenerSetKey)) {// At this stage, the eventSystemFlags input parameter is always 0.
    EventSystemFlags = IS_CAPTURE_PHASE = 1 << 2 as long as the listener is added in the capture phase.
    if (isCapturePhaseListener) {
      eventSystemFlags |= IS_CAPTURE_PHASE;
    }
    addTrappedEventListener(
      target,
      domEventName,
      eventSystemFlags,
      isCapturePhaseListener,
    );
    // Add to listenerSetlistenerSet.add(listenerSetKey); }}Copy the code

addTrappedEventListener

// Simplified version
function addTrappedEventListener(
  targetContainer: EventTarget,
  domEventName: DOMEventName,
  eventSystemFlags: EventSystemFlags,
  isCapturePhaseListener: boolean, isDeferredListenerForLegacyFBSupport? :boolean.) {
  // Create event listeners with priority, as outlined below
  let listener = createEventListenerWrapperWithPriority(
    targetContainer,
    domEventName,
    eventSystemFlags,
  );
    
  // ...

  let unsubscribeListener;

  // Add event listeners of different stages to native events
  if (isCapturePhaseListener) {
    // ...
    unsubscribeListener = addEventCaptureListener(
      targetContainer,
      domEventName,
      listener,
    );
  } else {
    // ...unsubscribeListener = addEventBubbleListener( targetContainer, domEventName, listener, ); }}Copy the code

CreateEventListenerWrapperWithPriority (create the listener with a priority)

export function createEventListenerWrapperWithPriority(targetContainer: EventTarget, domEventName: DOMEventName, eventSystemFlags: EventSystemFlags,) :Function {
  // Get the priority of the current native event from eventPriorities mentioned above
  const eventPriority = getEventPriorityForPluginSystem(domEventName);
  let listenerWrapper;
 
  // Different listener functions are provided according to different priorities
  switch (eventPriority) {
    case DiscreteEvent:
      listenerWrapper = dispatchDiscreteEvent;
      break;
    case UserBlockingEvent:
      listenerWrapper = dispatchUserBlockingUpdate;
      break;
    case ContinuousEvent:
    default:
      listenerWrapper = dispatchEvent;
      break;
  }
  
  // All listeners have the same input parameters:
  // (domEventName: DOMEventName, eventSystemFlags: EventSystemFlags, targetContainer: EventTarget, nativeEvent: AnyNativeEvent) => void
  // The first three arguments are provided by the current function, and the last argument is the Event object, the only input argument that native listeners will have
  return listenerWrapper.bind(
    null,
    domEventName,
    eventSystemFlags,
    targetContainer,
  );
}
Copy the code

AddEventCaptureListener/addEventBubbleListener (mount listener)

No further details

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

summary

Finally, on the root DOM node, each native event is bound to its corresponding priorityThe listener



Trigger event beginning

Up to this point, everything in this article can be understood as preparation for the composite event system until the page is rendered into a composite event system. After normal rendering is completed, when the browser invokes the native event, the synthetic event system will start to work 👷♂️. The listener mentioned above will receive the event of the browser, and then pass down the information to collect relevant events to simulate the triggering process of the browser and achieve the expected triggering effect. So how does the React composite event system work?

Results the overview


I want to give you a general idea before I explain it, just to give you an impression, and then you can look at it when you explain it.

The test Demo is as follows:



Three events were writtenonClick,onDrag,onPlaying, respectively corresponding to the events of three priorities in the composite event system, respectively triggering corresponding listeners.



The call stack after each event invocation is as follows from top to bottom:



We’ll focus on the first half of each call stack to understand how the React composite event system works.

The React update in the second half is beyond the scope of this article, so we won’t go into details.

Listener entry

We know from the call stack and above that when we click the button on the page. For the onClick example, the first thing to fire is the listener for the Click event that is mounted on the root DOM node, i.e. DispatchDiscreteEvent. Review, in this time “registered agent – createEventListenerWrapperWithPriority” as mentioned in section, will React according to the different priorities for different listener, the listener three kinds, respectively is:

  • DiscreteEvent listener: dispatchDiscreteEvent
  • User blocking event listener: dispatchUserBlockingUpdate
  • Continuous event or other event listener: dispatchEvent

First, the purpose of these three listeners is the same, the ultimate purpose is to collect events, event invocation. At the code level, as you can see from the call stack, the dispatchEvent (third type listener) function is called. The difference, however, is that what happens to listeners before they call dispatchEvent is different. Continuous events or other event listeners (the third type of listener) are called directly synchronously because they have the highest priority, whereas the other two types are different. So what we need to understand now is what the first two types of listeners do before calling dispatchEvent and why, and then focus on what dispatchEvent does. Due to the complexity of their content, the first two listeners, the author will first talk about the second type of listener “user blocking event listener” for readers to understand.

DispatchUserBlockingUpdate (user blocking event listener)

The content of the function is simple: a function runWithPriority is called, passing in the priority of the current task and the task you want to execute (the function). RunWithPriority marks the priority of the current task in a global static variable so that internal updates know which priority event is currently being executed. It is also possible to explain why the third type of listener calls dispatchEvent directly without any side effects because it is high priority and can be called synchronously.

// Simplified version
// The first three parameters are passed in when the event broker is registered,
// domEventName: indicates the name of the original event
// eventSystemFlags can only be 4 or 0, representing capture phase events and bubble phase events respectively
// container: application root DOM node
// nativeEvent: The Event object passed by the native listener
function dispatchUserBlockingUpdate(domEventName, eventSystemFlags, container, nativeEvent) {
  runWithPriority(
    UserBlockingPriority,
    dispatchEvent.bind(
      null,
      domEventName,
      eventSystemFlags,
      container,
      nativeEvent,
    ),
  )
}
Copy the code

DispatchDiscreteEvent (DiscreteEvent Listener)

The first type listener is a bit more complicated than the second type listener, and the following is a partial call chain from top to bottom

function dispatchDiscreteEvent(domEventName, eventSystemFlags, container, nativeEvent) {
  / / flushDiscreteUpdatesIfNeeded role is to clear the previously saved to perform the discrete tasks, including but not limited to, the discrete event before the trigger and useEffect callback,
  // To ensure that the state of the current discrete event is up to date
  // If you have a headache, you can pretend it doesn't exist
  flushDiscreteUpdatesIfNeeded(nativeEvent.timeStamp);
  
  // Create a new discrete update
  // The last four arguments are actually arguments to the first function argument
  // Call dispatchEvent(domEventName, eventSystemFlags, Container, nativeEvent)
  discreteUpdates(
    dispatchEvent,
    domEventName,
    eventSystemFlags,
    container,
    nativeEvent,
  );
}

export function discreteUpdates(fn, a, b, c, d) {
  // Marks that the event is currently being processed and stores the previous state
  const prevIsInsideEventHandler = isInsideEventHandler;
  isInsideEventHandler = true;
  try {
    // The current function only needs to focus on this line
    // Call the discrete update function in Scheduler
    return discreteUpdatesImpl(fn, a, b, c, d);
  } finally {
    // Continue if you were already in the event process
    isInsideEventHandler = prevIsInsideEventHandler;
    if(! isInsideEventHandler) { finishEventHandler(); }}}// A discrete update function in the react-Reconciler
// It does two things, the first is to call the corresponding discrete event, and the second is to update the updates that might be generated in the discrete event (if the timing is right)
// If you look at the discrete event listener call stack above, you will see that the two things here are respectively
// First thing: the synthetic event system collects real events and calls real events
// Second thing: Update any updates that might be generated in a real event
discreteUpdatesImpl = function discreteUpdates<A.B.C.D.R> (fn: (A, B, C) => R, a: A, b: B, c: C, d: D,) :R {
  // Add the current execution context state, which is used to determine the current situation, such as RenderContext, which indicates that the update is in the render phase
  // All context types https://github.com/facebook/react/blob/v17.0.0/packages/react-reconciler/src/ReactFiberWorkLoop.old.js#L249-L256
  const prevExecutionContext = executionContext;
  executionContext |= DiscreteEventContext;

  try {
    // This is the same as the second type of listener.
    return runWithPriority(
      UserBlockingSchedulerPriority,
      fn.bind(null, a, b, c, d),
    );
  } finally {
    Return to previous context
    executionContext = prevExecutionContext;
    // If there is no execution context, the execution is complete, and you can start updating the generated updates
    if (executionContext === NoContext) {
      // Flush the immediate callbacks that were scheduled during this batchresetRenderTimer(); flushSyncCallbackQueue(); }}}Copy the code


After the dispatchEvent

From the above description, I believe you must be cleardispatchEventIt is the end of all the fancy listeners that contain the core functionality of the synthetic event system.

The reader can start by reviewing the call stack shown aboveonClickFor example:



In factdispathEventAt the heart of this is invocationdispatchEventsForPluginsBecause it’s this function that firesEvent collection and event execution.

Before that, he would do a series of tedious scheduling and judgment of edge cases, which are of little reference value for the main process. Therefore, the author intends to skip the explanation of these contents and go straight to the theme, and only explain the source of its input.

dispatchEventsForPlugins

The fourth input parameter, targetInst, has a data type of Fiber or NULL, which is usually just Fiber, so you can ignore the possibility of null to avoid mental overload. In the case of the click event in the previous test Demo, targetInst is the Fiber node corresponding to the we clicked. AttemptToDispatchEvent React obtains the Fiber node in the attemptToDispatchEvent function that appears in the call stack, and there are two steps:

  1. Gets the object passed in from the listenerEventObject and getEvent.targetThe DOM node in, this DOM node is actually<button />.Access to functions
  2. Gets the object stored on the DOM nodeFiberNode,FiberThe node actually existsDOM.['__reactFiber$' + randomKey]The key value of.Access to functions.The corresponding assignment function
function dispatchEventsForPlugins(
  domEventName: DOMEventName, // Event name
  eventSystemFlags: EventSystemFlags, // Event processing phase, 4 = capture phase, 0 = bubble phase
  nativeEvent: AnyNativeEvent, // The listener's native input Event object
  targetInst: null | Fiber, // Event. target corresponds to the Fiber node of the DOM node
  targetContainer: EventTarget, // Root DOM node
) :void {
  // Get an event.target
  const nativeEventTarget = getEventTarget(nativeEvent);
  // Event queue, where collected events are stored
  const dispatchQueue: DispatchQueue = [];
  // Collect events
  extractEvents(
    dispatchQueue,
    domEventName,
    targetInst,
    nativeEvent,
    nativeEventTarget,
    eventSystemFlags,
    targetContainer,
  );
  // Execute the event
  processDispatchQueue(dispatchQueue, eventSystemFlags);
}
Copy the code

ExtractEvents (Collect events)

The content of the extractEvents is actually very simple, the extractEvents of several EventPlugin are called on demand, the purpose of which is the same, but different events may be generated for different events. We will with the most core is the most critical SimpleEventPlugin. ExtractEvents to explain

function extractEvents(
  dispatchQueue: DispatchQueue,
  domEventName: DOMEventName,
  targetInst: null | Fiber,
  nativeEvent: AnyNativeEvent,
  nativeEventTarget: null | EventTarget,
  eventSystemFlags: EventSystemFlags,
  targetContainer: EventTarget,
) :void {
  // Get the synthesized event name based on the native event name
  / / effect: onClick = topLevelEventsToReactNames. Get (' click ')
  const reactName = topLevelEventsToReactNames.get(domEventName);
  if (reactName === undefined) {
    return;
  }
  // The constructor of the default synthesis function
  let SyntheticEventCtor = SyntheticEvent;
  let reactEventType: string = domEventName;
  switch (domEventName) {
  	// Get the corresponding synthesized event constructor according to the native event name
  }

  // Is the capture phase
  constinCapturePhase = (eventSystemFlags & IS_CAPTURE_PHASE) ! = =0;
  if (
    enableCreateEventHandleAPI &&
    eventSystemFlags & IS_EVENT_HANDLE_NON_MANAGED_NODE
  ) {
    / /... Basically the same as below
  } else {
    // Scroll event does not bubble
    constaccumulateTargetOnly = ! inCapturePhase && domEventName ==='scroll';

    // core, get all the events of the current phase
    const listeners = accumulateSinglePhaseListeners(
      targetInst,
      reactName,
      nativeEvent.type,
      inCapturePhase,
      accumulateTargetOnly,
    );
    if (listeners.length > 0) {
      // Generate an Event object for the synthesized Event
      const event = new SyntheticEventCtor(
        reactName,
        reactEventType,
        null,
        nativeEvent,
        nativeEventTarget,
      );
      / / teamdispatchQueue.push({event, listeners}); }}}Copy the code

accumulateSinglePhaseListeners

// Simplified version
export function accumulateSinglePhaseListeners(
  targetFiber: Fiber | null,
  reactName: string | null,
  nativeEventType: string,
  inCapturePhase: boolean,
  accumulateTargetOnly: boolean.) :Array<DispatchListener> {
  // Capture phase synthesizes event names
  constcaptureName = reactName ! = =null ? reactName + 'Capture' : null;
  // The final synthesized event name
  const reactEventName = inCapturePhase ? captureName : reactName;
  
  const listeners: Array<DispatchListener> = [];

  let instance = targetFiber;
  let lastHostComponent = null;

  while(instance ! = =null) {
    const {stateNode, tag} = instance;
    // If it is a valid node, get its event
    if(tag === HostComponent && stateNode ! = =null) {
      lastHostComponent = stateNode;

      if(reactEventName ! = =null) {
        // Get the corresponding events stored in the Props on the Fiber node (if any)
        const listener = getListener(instance, reactEventName);
        if(listener ! =null) {
          / / team
          listeners.push(
            Simply return a {instance, listener, lastHostComponent} objectcreateDispatchListener(instance, listener, lastHostComponent), ); }}}// Scroll will not bubble
    if (accumulateTargetOnly) {
      break;
    }
    // Parent Fiber node, recursive up
    instance = instance.return;
  }
  
  // Returns a collection of listeners
  return listeners;
}
Copy the code

ProcessDispatchQueue (Execution event)

Before we look at the source code, let’s take a look at the data type of dispatchQueue, which is generally 1 in length.

interface dispatchQueue {
  event: SyntheticEvent
  listeners: {
    instance: Fiber,
    listener: Function.currentTarget: Fiber['stateNode']
  }[]
}[]
Copy the code

On the source code

export function processDispatchQueue(dispatchQueue: DispatchQueue, eventSystemFlags: EventSystemFlags,) :void {
  // Determine the current event phase using eventSystemFlags
  constinCapturePhase = (eventSystemFlags & IS_CAPTURE_PHASE) ! = =0;
  // Iterate over the composite event
  for (let i = 0; i < dispatchQueue.length; i++) {
    // Retrieve the synthesized Event object and the Event collection
    const {event, listeners} = dispatchQueue[i];
    // This function is responsible for calling events
    // If the capture phase Event is called in reverse order, otherwise it is called in positive order, passing in a synthesized Event object
    processDispatchQueueItemsInOrder(event, listeners, inCapturePhase);
  }
  
  // Throw an intermediate error
  rethrowCaughtError();
}
Copy the code

conclusion

The React composite event system is not particularly complex (compared with Concurrent mode code). The core idea is to use event agents to receive events and then schedule the actual event calls by React itself. At the heart of how React knows where to start collecting events are Fiber nodes stored in the real DOM, shuttled from the real to the virtual.

other

Differences from the 16.x version

React V17.0 RC release statement

Mention three strong changes in personal perception

1. The node mounted by the listener is changed from Document node to RootNode (root DOM node)

Related PR: Modern Event System: Add plugin handling and forked Paths #18195

Its main function is to bind and synthesize the influence range of the event system. Suppose I have a React application in a Web application and other applications also use the Document event monitoring, at this time, there is a high probability of mutual influence leading to errors of one party. This side effect is reduced when we bundle the composite event system into the root DOM node of the current React application.

2. Add all known event listeners when root is mounted, instead of adding listeners on demand in the completeWork stage

PR: Attaching Listeners to Roots and Portal Containers #19659

The main effect is to fix the createPortal event bubble problem, although the problem is not big, but the corresponding source code changes are really many.

3. Remove the event pool

Related PR:

  • Remove event pooling in the modern system #18216
  • Remove event pooling in the modern system #18969

It was originally designed to improve performance, but there was some mental overhead that led to asynchronous updates in events, so it was removed at this point without much performance impact.

Does Vue have its own event mechanism? How is React different?

Vue actually does a lot of its own content in the event, mainly for the convenience of developers to develop, such as various modifiers or instructions, its core operation mode is still dependent on its template compilation, is a more compile-time feature. Binding events are also mounted on the corresponding DOM one by one in the patch stage, rather than uniformly distributed by event proxy.

reference

  1. UI Events | W3C Working Draft, 04 August 2016
  2. The Event – | MDN Web API interface reference
  3. React V17.0 RC release statement
  4. Modern Event System: add plugin handling and forked paths #18195
  5. Attach Listeners Eagerly to Roots and Portal Containers #19659
  6. Remove event pooling in the modern system #18216
  7. Remove event pooling in the modern system #18969
  8. Learn more about the React synthetic event mechanism
  9. React events and the future