React takes a week and 8,000 words to learn more about the underlying principles of react synthetic events. Will native events prevent bubbles from blocking synthetic events?

Quote

In the previous two articles, we have spent a lot of time introducing react's setState method. When introducing setState synchronization asynchronous, we mentioned that react is asynchronous for this.state updates in react composite events, but synchronous for updates in native events, which means that react must be partially different from native events in the processing of synthetic events. This article then focuses on the synthetic events in react. Before we start, let's list some of the issues:

  • Learn about native event monitoring mechanisms? Why do I need an event proxy?
  • How does react implement synthetic events? What are the benefits of this?
  • The sequence in which composite events and native events are executed.
  • Does synthetic events block bubbles block native events? Does preventing bubbles with native events block synthetic events?

Before you start the article, you can think about these questions on your own. Assuming that these questions are encountered in an interview, how many answers can you give? So this article begins.

From Native Events

Although the core point of this article is to introduce react's synthetic events, it is necessary to understand the original events since the comparison between them is given at the beginning of the article. Given that some students may have forgotten about this knowledge, here is a brief review.

First let's review the event listener api addEventListener, which has the following basic syntax:

element.addEventListener(event, function, useCapture);

Where elementmeans the dom element you need to listen to; Events represent the type of events listened for, such as onclick,onblur; function is a callback after triggering an event. Write everything you need here. useCapture is a Boolean value indicating whether the capture phase is turned on or not, defaulting to false.

Take the following code structure as an example, when we click on space, we will inevitably go through the capture phase---- "target phase------" bubble phase:

Figure 1

Let's review this process with an example:

<div id="div">
    I am div
    <p id="p">
        I am p
        <span id="span">I am span</span>
    </p>
</div>
const div = document.querySelector("#div");
const p = document.querySelector("#p");
const span = document.querySelector("#span");
// Capture phase, where useCapture is set to true
div.addEventListener("click",()=>console.log("Capture Phase--div"),true);
p.addEventListener("click",()=>console.log("Capture Phase--p"),true);
// Target Stage
span.addEventListener("click",()=>console.log("Target Stage--span"));
// During the bubble phase, useCapture defaults to false, not written
div.addEventListener("click",()=>console.log("Capture Phase--div"));
p.addEventListener("click",()=>console.log("Capture Phase--p"));

Figure 2

Now that time monitoring is mentioned, three API s have to be mentioned, event.preventDefault, event.stopPropagation, and event.stopImmediatePropagation. Let's start by talking about stopPropagation, which is commonly used to prevent bubbles, such as when a parent and a child are bound to a click event, but I don't want the parent to be triggered during the bubbling phase when I click on the child, so by adding this method to the child's event callback, modify the code for the target phase in the above example to be:

span.addEventListener("click", (e) => {
    e.stopPropagation();
    console.log("Target Stage--span")
});

When you click on span, you will find that only span is output, and the div and p during the bubble phase are blocked from execution.

Figure 3

With respect to event.preventDefault, this method is often used to prevent default behavior of elements, such as clicking the a tag to perform default jumps of the a tag in addition to the click events that we bind to. Alternatively, a form expression that clicks Submit will pass the value of the form to the action at the specified address and refresh the page, and such behavior can be prevented by preventDefault.

Before introducing stopImmediatePropagation, we need to know that one of the great benefits of time listening over normal event binding is that event listening supports multiple behaviors for the same dom, but if it is normal event binding, the latter overrides the former:

span.onclick = ()=>console.log('Event Binding-1');
// Post-bound events override previous bindings
span.onclick = ()=>console.log('Time Binding-2');
// There will be no override for event monitoring, and the next two will execute
span.addEventListener("click", (e) => {
    console.log("event listeners-1")
});
span.addEventListener("click", (e) => {
    console.log("event listeners-2")
});

Figure 4

Now that event monitoring supports multiple bindings for the same dom, what do I do when I execute one listening and need to block all other listening? Now it's stopImmediatePropagation's turn to make a big difference. Take an example:

// Capture Phase
div.addEventListener("click", () => console.log("Capture Phase--div-1"), true);
div.addEventListener("click", () => console.log("Capture Phase--div-2"), true);
p.addEventListener("click", () => console.log("Capture Phase--p-1"), true);
p.addEventListener("click", () => console.log("Capture Phase--p-2"), true);
// Target Stage
span.addEventListener("click", (e) => {
    e.stopImmediatePropagation();
    console.log("Target Stage---span-1")
});
span.addEventListener("click", (e) => {
    console.log("Target Stage---span-2")
});
// bubbling phase
div.addEventListener("click", () => console.log("Capture Phase--div-1"));
div.addEventListener("click", () => console.log("Capture Phase--div-2"));
p.addEventListener("click", () => console.log("Capture Phase--p-1"));
p.addEventListener("click", () => console.log("Capture Phase--p-2"));

Figure 5

You can see that stopImmediatePropagation also prevents event bubbles, but in addition, it prevents other events from executing on the same dom.

So after talking about event monitoring, what is event proxy? In real life, most of the couriers we buy online to our company will be signed and received by the front desk instead of being sent to each of us separately. At this time, the front desk is equivalent to doing an agent thing, which originally requires different people to sign and receive separately, so it can be handled uniformly with the front desk agent.

Mapping to code, assuming there is a ul>li structure, we want to click on Li to display the text content of li, as you would if you were to bind each li:

<ul id="ul">
    <li onclick="handleClick(event)">1</li>
    <li onclick="handleClick(event)">2</li>
    <li onclick="handleClick(event)">3</li>
    <li onclick="handleClick(event)">4</li>
    <li onclick="handleClick(event)">5</li>
</ul>
const handleClick=(e)=>{
    console.log(e.target.innerHTML);
};

But with event code, we delegate the click behavior to ul, a parent element common to li, making the code clearer and simpler:

<ul id="ul">
    <li>1</li>
    <li>2</li>
    <li>3</li>
    <li>4</li>
    <li>5</li>
</ul>
const span = document.getElementById("span");
const handleClick = (e) => {
    span.innerHTML =`Now click on the ${e.target.innerHTML}individual li`;
};
const ul = document.querySelector("#ul");
ul.addEventListener("click", handleClick)

Figure 6

Although the event was proxied to ul, we still get the li of the actual operation through event.target. This reminds me of the 17 years when I was still writing JQ, when li was generated by dynamic traversal, if you bind an event to li you would find that the li actually did not exist at that time, which led to the eventual failure of event binding, and the event proxy cleverly solved this problem in the future with better performance.

OK, we're here about native event monitoring and time proxy, and this knowledge also helps us understand react synthetic events.

Analysis of the Principle of Synthetic Events (16.13.1)

Triple Pre-processing at Binding Stage

When we visit react's official documentation on composite events, the first information we get is that react generates composite events uniformly through the SyntheticEvent wrapper. It is important to note that react does not create a system of events independently, and that all synthetic events are essentially dependent on native events. The wrapper react also normalize s native events to smooth out differences in event handling between different browsers.

Remember the event agent we mentioned when we introduced the native event? For performance optimization reasons, synthetic events in react do a similar process. Most synthetic events (not all) are ultimately mounted on the document, not on the real dom of the component that you define. First, let's get a feel for this concept. Next, let's look at the binding and execution phases of react composite events at the source level.

Note that during my review of the material, there are differences in the handling of synthetic events between react versions. For example, events in react 17 are no longer registered on the document, but on the container s bound by your components. I use 16.13.1 as the source code here, and the source code in this article can be found in the react-dom.development.js file.

<button className="button" onClick={this.handleClick}>click</button>

As we have already mentioned above, react composite events actually depend on native events, so the type of synthetic events naturally corresponds to one-to-one relationship with native events. After all, the click event of react is the onClick of the hump, while native onclick. As an example, when react is rendered to a button, a synthetic event is found in the props of this component. In theory, what react does at this point is register the operation, find the native event type for the onClick, and do the subsequent packaging action.

For event types, an event classification plug-in named injectEventPluginsByName is provided in react, which executes self-injection during the initialization phase, and react can be named to classify different event types:

// Global object for copy injectedNamesToPlugins
var namesToPlugins = {};

// Here injectedNamesToPlugins is the following self-invoking injection of different event plug-in objects, I deleted some code that does not affect understanding
function injectEventPluginsByName(injectedNamesToPlugins) {
  var isOrderingDirty = false;
  // Traverse all plug-in objects
  for (var pluginName in injectedNamesToPlugins) {
    if (!injectedNamesToPlugins.hasOwnProperty(pluginName)) {
      continue;
    }
    // Get value s by plugin key
    var pluginModule = injectedNamesToPlugins[pluginName];

    if (!namesToPlugins.hasOwnProperty(pluginName) || namesToPlugins[pluginName] !== pluginModule) {
      // Assign plug-in objects to global objects namesToPlugins in key-value order
      namesToPlugins[pluginName] = pluginModule;
      isOrderingDirty = true;
    }
  }

  if (isOrderingDirty) {
    recomputePluginOrdering();
  }
}
// Initialization phase is self-executing, injecting different types of event plug-ins
injectEventPluginsByName({
  SimpleEventPlugin: SimpleEventPlugin,
  EnterLeaveEventPlugin: EnterLeaveEventPlugin,
  ChangeEventPlugin: ChangeEventPlugin,
  SelectEventPlugin: SelectEventPlugin,
  BeforeInputEventPlugin: BeforeInputEventPlugin
});

Out of curiosity, I went directly to the initialization breakpoint, where I can see that SimpleEventPlugin contains two objects, eventTypes and extractEvents, for each event plug-in type

Figure 7

Whereas extractEvents is the function that the event will ultimately execute, EvetTypes contains information about the corresponding native event for the composite event:

Figure 8

In the recomputePluginOrdering method in the code above, let's go on and find the following:

// These two are also global objects
var registrationNameModules = {};
var registrationNameDependencies = {};

function publishRegistrationName(registrationName, pluginModule, eventName) {
  // Mapping Composite Event Names to Event Plugins
  registrationNameModules[registrationName] = pluginModule;
  // Establish mapping of composite event names to native events
  registrationNameDependencies[registrationName] = pluginModule.eventTypes[eventName].dependencies;

  {
    var lowerCasedName = registrationName.toLowerCase();
    possibleRegistrationNames[lowerCasedName] = registrationName;

    if (registrationName === 'onDoubleClick') {
      possibleRegistrationNames.ondblclick = registrationName;
    }
  }
}

An important thing to do in this approach is assign values to two global objects, where registrationNameModules are used to store the mapping of composite event names to event plug-ins, such as which event plug-in a composite event belongs to, and through breakpoints we can see this structure:

Figure 9

As we have said earlier, each event type object contains two attributes, eventTypes and extractEvents, so the structure of the above diagram is essentially the same:

{
 onClick: SimpleEventPlugin,
 onClickCapture: SimpleEventPlugin,
 onChange: ChangeEventPlugin,
 onChangeCapture: ChangeEventPlugin,
 onMouseEnter: EnterLeaveEventPlugin,
 ...
}

RegisrationNameDependencies are used to preserve the mapping relationship between composite events and native events, such as which combinations of native events simulate a composite event, and the same segment of points:

Figure 10

So its structure is the same as:

{
  onClick: ['click'],
	onClickCapture: ['click'],
  onClose: ['close'],
	onCloseCapture: ['close'],
  onChange: ['blur', 'change', 'click', 'focus', 'input', 'keydown', 'keyup', 'selectionchange']
	onChangeCapture: ['blur', 'change', 'click', 'focus', 'input', 'keydown', 'keyup', 'selectionchange']
}

You'll be surprised to find that there are eight native events corresponding to a composite event onChange, which means that multiple native event combinations are used at the bottom of react to simulate a native event, and that's why react can smooth out differences between browser events and achieve the same interaction with the same composite event.

We actually omitted a lot of intermediate code above, but in summary, we injected the Composite Event Plug-in and then traversed the composite event several times, like stripping an onion, through each event type and each composite event under each event type, resulting in multiple global objects for subsequent registration.

OK, the preconditions are finished, then an onClick property is defined on a component, how react binds it to a document, and now we formally introduce the binding phase.

3 Binding Stages

When a component is in the initialization or update phase, react always rechecks the props property on the component to see if it corresponds to the registrationNameModules. If so, this property is a composite event name. Here's an example of updating the component (you want to see how initialization works with setInitialDOMProperties):

function diffHydratedProperties(domElement, tag, rawProps, parentNamespace, rootContainerElement) {

  switch (tag) {
    case 'video':
    case 'audio':
      // Create listener for each media event
      for (var i = 0; i < mediaEventTypes.length; i++) {
        // Note that here the binding event passes the dom itself
        trapBubbledEvent(mediaEventTypes[i], domElement);
      }
      break;

    case 'source':
      trapBubbledEvent(TOP_ERROR, domElement);
      break;

    case 'select':
      ensureListeningTo(rootContainerElement, 'onChange');
      break;

    case 'textarea':
      ensureListeningTo(rootContainerElement, 'onChange');
      break;
  }
  // Traversing props
  for (var propKey in rawProps) {
    if (propKey === CHILDREN) {
      // Composite Event to Plugin Mapping If this propKey is found, it means it is a composite event
    } else if (registrationNameModules.hasOwnProperty(propKey)) {
      if (nextProp != null) {
        // Register the event, note that the document is passed here
        ensureListeningTo(rootContainerElement, propKey);
      }
    } else if (){
      // ...
    }
  }
  return updatePayload;
}

This method is very long, I delete a lot of extra code, and extract the information here. We said earlier that most of the events are ultimately mounted on the document, because in fact, there is switch here, media labels like video s, documents can't simulate their events, so the dom passed by the binding is actually a domElement, which means the events of your elements are too special. I can't help you. You still tie yourself up.

Out of curiosity, I followed the trapBubbledEvent approach, which follows a general process:

function trapBubbledEvent(topLevelType, element) {
  // Call a method named trapEventForPluginEventSystem
  trapEventForPluginEventSystem(element, topLevelType, false);
}
// The only difference with this method is that it is captured as true
function trapCapturedEvent(topLevelType, element) {
  trapEventForPluginEventSystem(element, topLevelType, true);
}

function trapEventForPluginEventSystem(container, topLevelType, capture) {
  var listener;
	// Generate final event listening callbacks based on event type and level
  switch (getEventPriorityForPluginSystem(topLevelType)) {
    case DiscreteEvent:
      listener = dispatchDiscreteEvent.bind(null, topLevelType, PLUGIN_EVENT_SYSTEM, container);
      break;

    case UserBlockingEvent:
      listener = dispatchUserBlockingUpdate.bind(null, topLevelType, PLUGIN_EVENT_SYSTEM, container);
      break;

    case ContinuousEvent:
    default:
      listener = dispatchEvent.bind(null, topLevelType, PLUGIN_EVENT_SYSTEM, container);
      break;
  }
  // Get Event Name
  var rawEventName = getRawEventName(topLevelType);

  // Final event listening methods are invoked separately to determine if capture is the phase
  if (capture) {
    addEventCaptureListener(container, rawEventName, listener);
  } else {
    addEventBubbleListener(container, rawEventName, listener);
  }
}

// Let's look at the implementation of capture and non-capture phases
function addEventBubbleListener(element, eventType, listener) {
  // You can see that it's really directly bound to the dom and captured as false
  element.addEventListener(eventType, listener, false);
}

function addEventCaptureListener(element, eventType, listener) {
  // Also bound to the original God itself, but captured as true
  element.addEventListener(eventType, listener, true);
}

There are three method calls:

  • trapBubbledEvent and trapCapturedEvent are brothers who register for capturing and not capturing different types of events
  • trapEventForPluginEventSystem is a method that is called by both of the above methods. Inside this method, you will find that react also defines different event levels based on the event name type, resulting in different levels of event callback, or listener, but eventually they call different methods based on capture.
  • Whether it's an addEventBubbleListener or an addEventCaptureListener, their binding execution is an addEventListener method that we're no longer familiar with, except that elementation is not a document but an element itself. So you'll find that tags like video s bind events to themselves, not to documents!!

OK, the code goes on to look at the rawProps traversal in the previous section of code, where registrationNameModules.hasOwnProperty(propKey) is to verify that your key exists in the registrationNameModules (mapping of composite events to event plug-ins). If so, you must be a composite event, then let's bind it for you and execute it:

ensureListeningTo(rootContainerElement, propKey);

Note that the switch before the special event has been specialized, so it must be a plain and document-capable event. In addition, some students do not understand who the document and container mentioned above mean, so let's break a few points to demonstrate:

Figure 11

In the figure above, I break point directly at the createElement and take a look at the rootContainerElement, you will see that the so-called container is actually the container we created the application:

ReactDOM.render(element, container[, callback])

So in my demo, it's actually a div with the id root:

<div id="root"></div>

So who is the document mentioned above? There is code in the source specifically to get the document:

var ownerDocument = getOwnerDocumentFromRootContainer(rootContainerElement)

We can output it from a breakpoint and see that it is actually html:

Figure 12

Figure 13

Let's go back to the event binding function ensureListeningTo and see what it does:

// rootContainerElement is a container element and registrationName is a composite event name
function ensureListeningTo(rootContainerElement, registrationName) {
  // Determine if our rootContainerElement is a document or a snippet
  var isDocumentOrFragment = rootContainerElement.nodeType === DOCUMENT_NODE || rootContainerElement.nodeType === DOCUMENT_FRAGMENT_NODE;
  // As you've said before, the rootContainerElement here is a normal div, so it must be false. Take rootContainerElement.ownerDocument
  var doc = isDocumentOrFragment ? rootContainerElement : rootContainerElement.ownerDocument;
  // The doc here is the document
  legacyListenToEvent(registrationName, doc);
}

Some students may see that the passed parameter is rootContainerElement, just want to say not bound to document, how to pass container. In fact, this method is to determine if the dom node you passed is a document, if it is, use it directly, if not, take rootContainerElement.ownerDocument. The above code, because we know that the rootContainerElement is a div container, so isDocumentOrFragment must be false, so the doc value will naturally be rootContainerElement.ownerDocument. We can also breakpoint to see what this property outputs, and the results are as follows:

Figure 14

Actually, it's still a document... In summary, the ensureListeningTo method is to make sure that your event ultimately helps the document!

The method finally executes legacyListenToEvent, so take a look at the code:

// The registrationName composite event name mountAt is a document
function legacyListenToEvent(registrationName, mountAt) {
  // Get the object on the document that has been listened on so far
  var listenerMap = getListenerMapForElement(mountAt);
  // Gets the array of native events corresponding to the composite event name
  var dependencies = registrationNameDependencies[registrationName];
	// Traverse through the native object array and call legacyListenToTopLevelEvent to mount
  for (var i = 0; i < dependencies.length; i++) {
    var dependency = dependencies[i];
    legacyListenToTopLevelEvent(dependency, mountAt, listenerMap);
  }
}

This method should be clear to you when you look at the comment, registrationNameDependencies is the mapping of the synthetic event name and the corresponding native event that we explained earlier, and then traverse to start the registration. OK, let's move on to the registrationNameDependencies code:

// topLevelType native event name mountAt is the object that document listenerMap ducument has listened on at this time
function legacyListenToTopLevelEvent(topLevelType, mountAt, listenerMap) {
  // Don't repeat what you've listened on, only execute internal code if you haven't.
  if (!listenerMap.has(topLevelType)) {
    switch (topLevelType) {
      case TOP_SCROLL:
        trapCapturedEvent(TOP_SCROLL, mountAt);
        break;

      case TOP_FOCUS:
      case TOP_BLUR:
        trapCapturedEvent(TOP_FOCUS, mountAt);
        trapCapturedEvent(TOP_BLUR, mountAt); // We set the flag for a single dependency later in this function,
			// I deleted some code here
      default:
        // By default, listen on the top level to all non-media events.
        // Media events don't bubble so adding the listener wouldn't do anything.
        var isMediaEvent = mediaEventTypes.indexOf(topLevelType) !== -1;

        if (!isMediaEvent) {
          trapBubbledEvent(topLevelType, mountAt);
        }

        break;
    }

    listenerMap.set(topLevelType, null);
  }
}

The legacyListenToTopLevelEvent method looks very long, but what it does is very simple. You can tell from the known listenerMap if the current native event was not bound before it was executed. If it is not bound, the binding will be executed. There are only two binding event methods, trapCapturedEvent and trapBubbledEvent, which can be searched directly in this article. You'll find that these are the two methods explained above, and you end up with the sentence element.addEventListener(eventType, listener, true/false).

So here, we've explained the whole process of the event listening phase. You know how different composite events correspond to native events, and how they end up hanging on the document or the element itself, so let's move on to the execution phase.

III. III. Implementation Phase

Before we talk about the execution phase, we have to think about what the execution phase does, and in the binding phase, we know that the final react will still execute the following code:

element.addEventListener(eventType, listener, false);

The listener here is supposed to be the callback executed after the event is triggered, so how is this listener generated? How does it relate to the actual execution callback I wrote in the react code? This will have to go back to the trapEventForPluginEventSystem method explained above.

In the trapEventForPluginEventSystem method, we say dispatchDiscreteEvent, dispatchUserBlockingUpdate, or dispatchEvent will be called to generate listener s, depending on the event priority, respectively. However, when I try to follow the previous two methods, I find that both methods ultimately call dispatchEvent, taking dispatchUserBlockingUpdate as an example:

listener = dispatchUserBlockingUpdate.bind(null, topLevelType, PLUGIN_EVENT_SYSTEM, container);

function dispatchUserBlockingUpdate(topLevelType, eventSystemFlags, container, nativeEvent) {
  // Essentially the listener generated by calling dispatchEvent.bind()
  runWithPriority(UserBlockingPriority, dispatchEvent.bind(null, topLevelType, eventSystemFlags, container, nativeEvent));
}

So we just need to look at dispatchEvent, code it:

/**
 * 
 * @param {*} topLevelType Native Event Name
 * @param {*} eventSystemFlags A number constant 1
 * @param {*} container Container to listen for events
 * @param {*} nativeEvent event object
 * @returns 
 */
function dispatchEvent(topLevelType, eventSystemFlags, container, nativeEvent) {
	// Remove redundant code
  {
    // Finally, another han'shu call was made
    dispatchEventForLegacyPluginEventSystem(topLevelType, eventSystemFlags, nativeEvent, null);
  }
} 

function dispatchEventForLegacyPluginEventSystem(topLevelType, eventSystemFlags, nativeEvent, targetInst) {
  var bookKeeping = getTopLevelCallbackBookKeeping(topLevelType, nativeEvent, targetInst, eventSystemFlags);

  try {
    // Event queue being processed in the same cycle allows
    // `preventDefault`.
    batchedEventUpdates(handleTopLevel, bookKeeping);
  } finally {
    releaseTopLevelCallbackBookKeeping(bookKeeping);
  }
}

We omitted the redundant code in dispatchEvent and found that it eventually executed the dispatchEvent ForLegacy PluginEventSystem to follow up. This method did three things altogether, fetching the bookKeeping object, calling the batchedEventUpdates batched event (essentially calling handleTopLevel), And execute the releaseTopLevelCallbackBookKeeping storage bookKeeping method after the call is completed to achieve the reuse purpose, but continue to look at the handleTopLevel implementation:

function handleTopLevel(bookKeeping) {
  var targetInst = bookKeeping.targetInst; 
  var ancestor = targetInst;
	// I've been doing this all the time, traversing to save the existing dom structure
  do {
    if (!ancestor) {
      var ancestors = bookKeeping.ancestors;
      ancestors.push(ancestor);
      break;
    }
		// Find the parent node of the current node information, bubbling up
    var root = findRootContainerNode(ancestor);

    if (!root) {
      break;
    }

    var tag = ancestor.tag;
    if (tag === HostComponent || tag === HostText) {
      bookKeeping.ancestors.push(ancestor);
    }
    ancestor = getClosestInstanceFromNode(root);
  } while (ancestor);
	
  for (var i = 0; i < bookKeeping.ancestors.length; i++) {
    targetInst = bookKeeping.ancestors[i];
    var eventTarget = getEventTarget(bookKeeping.nativeEvent);
    var topLevelType = bookKeeping.topLevelType;
    var nativeEvent = bookKeeping.nativeEvent;
    var eventSystemFlags = bookKeeping.eventSystemFlags; // If this is the first ancestor, we mark it on the system flags

    if (i === 0) {
      eventSystemFlags |= IS_FIRST_ANCESTOR;
    }
		// The method that ultimately generates the composite event
    runExtractedPluginEventsInBatch(topLevelType, targetInst, nativeEvent, eventTarget, eventSystemFlags);
  }
}

The role of the handleTopLevel method is explained in its comments, considering that event callbacks may change the existing DOM structure, causing a deep traversal to save the existing component hierarchy first. From the code explanation, findRootContainerNode is obviously looking for the parent element of the current node element. If the parent continues the while loop, it is obviously doing a bubble operation, and then the for loop below is calling runExtractedPluginEventsInBatch in turn to generate a composite event based on the order of the bubbles.

function runExtractedPluginEventsInBatch(topLevelType, targetInst, nativeEvent, nativeEventTarget, eventSystemFlags) {
  // Generate Composite Events
  var events = extractPluginEvents(topLevelType, targetInst, nativeEvent, nativeEventTarget, eventSystemFlags);
  // Execution Event
  runEventsInBatch(events);
}

Maybe you're a little familiar with extractPluginEvents now, but when we introduced the composite event name and event plug-in mapping property registrationNameModules earlier in the article, we mentioned that each object has an extractEvents property that binds to the generated composite event in order to call back the events written in our code:

function extractPluginEvents(topLevelType, targetInst, nativeEvent, nativeEventTarget, eventSystemFlags) {
  var events = null;

  for (var i = 0; i < plugins.length; i++) {
    var possiblePlugin = plugins[i];
    if (possiblePlugin) {
      // Gets the extractEvents method of each event plug-in for generating composite events
      var extractedEvents = possiblePlugin.extractEvents(topLevelType, targetInst, nativeEvent, nativeEventTarget, eventSystemFlags);

      if (extractedEvents) {
        events = accumulateInto(events, extractedEvents);
      }
    }
  }

  return events;
}

This method has a global object plugins, which is actually an array of event plugin objects:

var plugins = [LegacySimpleEventPlugin, LegacyEnterLeaveEventPlugin, ...];

So this is really about traversing the event plug-in and trying to generate the corresponding composite event, so we can look at the internal implementation of extractEvents:

extractEvents: function (topLevelType, targetInst, nativeEvent, nativeEventTarget, eventSystemFlags) {
  var dispatchConfig = topLevelEventsToDispatchConfig.get(topLevelType);

  if (!dispatchConfig) {
    return null;
  }

  var EventConstructor;

  switch (topLevelType) {
    case TOP_KEY_PRESS:
      if (getEventCharCode(nativeEvent) === 0) {
        return null;
      }
    case TOP_KEY_DOWN:
    case TOP_KEY_UP:
      EventConstructor = SyntheticKeyboardEvent;
      break;

    case TOP_BLUR:
    case TOP_FOCUS:
      EventConstructor = SyntheticFocusEvent;
      break;
    default:
      EventConstructor = SyntheticEvent;
      break;
  }

  var event = EventConstructor.getPooled(dispatchConfig, targetInst, nativeEvent, nativeEventTarget);
  accumulateTwoPhaseDispatches(event);
  return event;
}

I omitted some case branching in the code, but in either case, methods like SyntheticFocusEvent and SyntheticKeyboardEvent will appear. Looking at the implementation code, I see that these builders are actually subclasses inherited from SyntheticEvent.extension and executed at the last switch default in the code. SyntheticEvent is also given this constructor by default.

The call to EventConstructor.getPooled to get synthetic event instances from the event pool immediately after getting the constructor also explains why react started saying that synthetic events were generated by the SyntheticEvent wrapper.

We can continue with accumulateTwoPhaseDispatches in the code above:

function accumulateTwoPhaseDispatchesSingle(event) {
  if (event && event.dispatchConfig.phasedRegistrationNames) {
    traverseTwoPhase(event._targetInst, accumulateDirectionalDispatches, event);
  }
}

// Simulate two-phase traversal to capture/bubble event assignment.
function traverseTwoPhase(inst, fn, arg) {
  var path = [];
  while (inst) {
    path.push(inst);
    inst = getParent(inst);
  }
  var i;
  for (i = path.length; i-- > 0;) {
    fn(path[i], 'captured', arg);
  }
  for (i = 0; i < path.length; i++) {
    fn(path[i], 'bubbled', arg);
  }
}

function accumulateDirectionalDispatches(inst, phase, event) {
  var listener = listenerAtPhase(inst, event, phase);
  if (listener) {
    event._dispatchListeners = accumulateInto(event._dispatchListeners, listener);
    event._dispatchInstances = accumulateInto(event._dispatchInstances, inst);
  }
}

The traverseTwoPhase method is of paramount importance, and the official comment on this function is particularly clear. By traversing forward and backward to simulate the event capture and event bubbling phases, the fn it executes is actually the function accumulateDirectionalDispatches, whose internal main responsibility is to find callbacks to event definitions on nodes. And add it to the _of the generated composite event event event In the dispatchListeners property, up to this point, we have completed the generation of composite events (the difference in the order in which onClickCapture and onClick execute was originally bound to simulations by traversal in different directions during the generation phase of composite events) and linked to the callback we defined for composite events.

Let's go back to the runExtractedPluginEventsInBatch method again and take a look at the runEventsInBatch method.

function runEventsInBatch(events) {
  var processingEventQueue = eventQueue;
  eventQueue = null;
	// Remove redundant code for final execution
  forEachAccumulated(processingEventQueue, executeDispatchesAndReleaseTopLevel);
}


var executeDispatchesAndReleaseTopLevel = function (e) {
  return executeDispatchesAndRelease(e);
};

The code is simple, and then my eyes are attracted by the executeDispatchesAndReleaseTopLevel method, which translates to event execution dispatch and release, so we continue to follow the executeDispatchesAndRelease method:

var executeDispatchesAndRelease = function (event) {
  // If events exist, then dispatch events are executed sequentially
  if (event) {
    executeDispatchesInOrder(event);
    if (!event.isPersistent()) {
      event.constructor.release(event);
    }
  }
};

function executeDispatchesInOrder(event) {
  var dispatchListeners = event._dispatchListeners;
  var dispatchInstances = event._dispatchInstances;
  {
    validateEventDispatches(event);
  }
  if (Array.isArray(dispatchListeners)) {
    for (var i = 0; i < dispatchListeners.length; i++) {
      if (event.isPropagationStopped()) {
        break;
      }

      executeDispatch(event, dispatchListeners[i], dispatchInstances[i]);
    }
  }
}

After further follow-up, the executeDispatchesInOrder method was finally located, and we even saw events associated with event callbacks and synthetic events during the generation phase of synthetic events. The dispatchListeners object, which is executed sequentially within this method in the binding order.

After a lengthy code trace, we have roughly completed the three phases of composite event generation, binding, and execution.

The Execution Order of Synthetic Events and Primary Events

After learning about composite events, I can't help but wonder who will execute them first if I bind them to a dom at the same time as the original one? Take an example:

class Echo extends Component {
  componentDidMount() {
    const parentDom = ReactDOM.findDOMNode(this);
    const childrenDom = parentDom.querySelector(".button");
    childrenDom.addEventListener('click', this.onDomClick, false);
  }
  onDomClick = (e) => {
    console.log('Native Events click');
  }
  onReactClick = () => {
    console.log('Composite Events click');
  }
  render() {
    return (
      <div>
        <button className="button" onClick={this.onReactClick}>click</button>
      </div>
    )
  }
}

Figure 14

Why are native events faster than synthetic events? From the source code analysis above, it is easy to imagine that the native event was triggered before the document was bubbled, and then it was time for the document to start event dispatch, traverse the array to execute the callback of the react composite event, which makes the composite event slow and reasonable.

Ouch? So what if we bind both native and synthetic capture events to a dom? So according to this statement, document s are at the top level, should synthetic capture events be performed earlier than native capture events? Take an example:

class Echo extends Component {
  componentDidMount() {
    const parentDom = ReactDOM.findDOMNode(this);
    const childrenDom = parentDom.querySelector(".button");
    childrenDom.addEventListener('click', this.onDomClick, true);
  }
  onDomClick = (e) => {
    console.log('Native Event Capture click');
  }
  onReactClick = () => {
    console.log('Composite Event Capture click');
  }
  render() {
    return (
      <div>
        <button className="button" onClickCapture={this.onReactClick}>click</button>
      </div>
    )
  }
}

Figure 15

How can native events precede the capture phase of synthetic events?????

In the analysis of source code for synthesized event generation, we introduce the handleTopLevel method, which mentions that synthesized events are synthesized event callbacks with the same name that are continuously collected upward by current node bubbles, and in traverseTwoPhase, there is no so-called synthesized event capture at all by two traverses, positive and negative. In fact, all rely on bubbles to collect events, control the traversal order, to simulate the capture and bubbles of the event execution order!!!

function traverseTwoPhase(inst, fn, arg) {
  var path = [];
  while (inst) {
    path.push(inst);
    inst = getParent(inst);
  }
  var i;
  // Capture Reverse Editing
  for (i = path.length; i-- > 0;) {
    fn(path[i], 'captured', arg);
  }
  for (i = 0; i < path.length; i++) {
    // Bubble traversal forward
    fn(path[i], 'bubbled', arg);
  }
}

So the capture of synthetic events, after all, is after the bubbling of native events, because I don't bubbled events you haven't collected them, what did you capture?

So in summary, synthetic events catch or bubble later than native events, combined with previous source analysis, it is very reasonable!! Let's take this example to make a deeper impression:

class Echo extends Component {
  componentDidMount() {
    const parentDom = ReactDOM.findDOMNode(this);
    const childrenDom = parentDom.querySelector(".button");

    childrenDom.addEventListener('click', this.onDomChildClick, false);
    childrenDom.addEventListener('click', this.onDomChildClickCapture, true);
    parentDom.addEventListener('click', this.onDomParentClick, false);
    parentDom.addEventListener('click', this.onDomParentClickCapture, true);
  }
  onDomChildClick = (e) => {
    console.log('Native Events child--Bubble');
  }
  onDomChildClickCapture = (e) => {
    console.log('Native Events child--capture');
  }
  onDomParentClick = (e) => {
    console.log('Native Events parent--Bubble');
  }
  onDomParentClickCapture = (e) => {
    console.log('Native Events parent--capture');
  }
  onReactChildClick = () => {
    console.log('Composite Events child--capture');
  }
  onReactParentClick = () => {
    console.log('Composite Events parent--capture');
  }
  render() {
    return (
      <div className="parent" onClickCapture={this.onReactParentClick}>
        <button className="button" onClick={this.onReactChildClick}>click</button>
      </div>
    )
  }
}

Figure 16

Summarize the execution order of composite events and native events:

  • Synthetic events, whether in the bubble or capture phase, are later than the native event bubble phase
  • Whether synthetic or native, the bubble phase is later than the capture phase

Will Wu Xu block the execution of synthetic events by preventing the bubble of native events?

Believe this, you should answer without thinking. If you stop bubbles in a native event, then the event will not be able to execute the document, the synthetic event will not have the opportunity to execute, or the above example, we will modify the following code:

onDomChildClick = (e) => {
  e.stopPropagation()
  console.log('Native Events child--Bubble');
}

Blocking bubbles during the native bubbling phase of a child element can be seen as follows, and the entire composite event is blocked.

Figure 17

The reason is that the answer has been given in the executeDispatchesInOrder method of source analysis above:

if (Array.isArray(dispatchListeners)) {
  for (var i = 0; i < dispatchListeners.length; i++) {
    // If bubbles are prevented, break directly out of the cycle
    if (event.isPropagationStopped()) {
      break;
    }
    executeDispatch(event, dispatchListeners[i], dispatchInstances[i]);
}}

And the other way around? Will it affect the original event if we stop the bubble during the bubble phase of the synthetic event? I think you already have an answer in your mind:

class Echo extends Component {
  componentDidMount() {
    const parentDom = ReactDOM.findDOMNode(this);
    const childrenDom = parentDom.querySelector(".button");
    childrenDom.addEventListener('click', this.onDomChildClick, false);
    parentDom.addEventListener('click', this.onDomParentClick, false);
  }
  onDomChildClick = (e) => {
    console.log('Native Events child--Bubble');
  }
  onDomParentClick = (e) => {
    console.log('Native Events parent--Bubble');
  }
  onReactChildClick = (e) => {
    e.stopPropagation()
    console.log('Composite Events child--Bubble');
  }
  onReactParentClick = (e) => {
    console.log('Composite Events parent--Bubble');
  }
  render() {
    return (
      <div className="parent" onClick={this.onReactParentClick}>
        <button className="button" onClick={this.onReactChildClick}>click</button>
      </div>
    )
  }
}

Figure 18

So here we explain the effect of preventing bubbles from synthesizing events on native events, but in practice, we try not to mix native events with synthetic events.

Ground Perennial

So here, I have roughly elaborated the point of view that I want to express in this paper. From the initial knowledge concept to the point of combing knowledge by reading the source code, I am ashamed to spend a week of scattered time around. But in the end, I know more or less about this area of knowledge, at least during the interview if an interviewer asks, at least I can chat a little. Whether the native events in this article will block the synthetic events is also the question that my former colleague was asked during the interview with Golden Mountain.

Posted by Yves on Sat, 27 Nov 2021 09:38:10 -0800