Once I was asked about the React event mechanism, I did not know much about it, because I used Vue for most of my work, so I was only familiar with React. I didn’t learn the specific implementation logic, but I couldn’t say I didn’t know much about it. Fortunately, I knew the event mechanism of Vue. React and Vue React React and Vue React React

Later, I thought it was not that simple, so I searched related articles online, and found that it was really too simple. Vue compiled templates, parsed event instructions, and attached events and event callbacks to the VNode tree. In the creation and update stages of patch, this VNode tree will be processed. After obtaining the additional event information on each Vnode, the native DOM API can be called to register or remove corresponding events. The process is relatively clear, while React implements a set of event mechanism independently

This article analyzes React V16.5.2 based on the source code

The basic flow

In the react source react – dom/SRC/events/ReactBrowserEventEmitter. At the beginning of the js file, so a large section of the notes:

/** * Summary of `ReactBrowserEventEmitter` event handling: * * - Top-level delegation is used to ...... *... * * +------------+ . * | DOM | . * +------------+ . * | . * v . * +------------+ . * | ReactEvent | . * | Listener | . *  +------------+ . +-----------+ * | . +--------+|SimpleEvent| * | . | |Plugin | * +-----|------+ . v +-----------+ * | |  | . +--------------+ +------------+ * | +-----------.--->|EventPluginHub| | Event | * | | . | | +-----------+ | Propagators| * | ReactEvent | . | | |TapEvent | |------------| * | Emitter | . | |<---+|Plugin | |other plugin| * | | . | | +-----------+ | utilities | * | +-----------.--->| | +------------+ * | | | . +--------------+ * +-----|------+ . ^ +-----------+ * | . | |Enter/Leave| * + . +-------+|Plugin | * +-------------+ . +-----------+ * | application | . * |-------------| . * | | . * | | . * +-------------+ . * . * React Core . General Purpose Event Plugin System */
Copy the code

This comment was I omitted the first paragraph of text, it mainly in the description about the React event mechanism, which is the code in this file to do some things, perhaps it means that trust is one of the popular browser events optimization strategy, then React to take over this matter, and also close to eliminate the differences between the browser, The cross-browser development experience is based on the use of EventPluginHub, which is responsible for scheduling events, synthesizing events, and creating and destroying them as a pool of objects. The following structure is a graphical description of the event mechanism

According to this note, the following points can be extracted:

  • ReactEvents use the mechanism of event delegate, the general function of event delegate is to reduce the number of registered events on the page, reduce memory overhead, optimize browser performance,ReactThat’s one of the goals, but it’s also to be able to manage events better, and in fact,ReactAll events are eventually delegateddocumentThe topDOMon
  • Now that all events have been delegateddocumentThere must be some sort of management mechanism, and all events are triggered and called back in a fifO queue
  • Now that we’ve already taken over events, it’s kind of a waste not to do something extra with them, soReactThere exists its own composite event (SyntheticEvent), synthesized by the correspondingEventPluginResponsible for composition, different types of events are composed of differentpluginComposition, for exampleSimpleEvent Plugin,TapEvent PluginEtc.
  • To further improve the performance of the event, theEventPluginHubThis thing is responsible for the creation and destruction of composite event objects

The following code is used as an example for analysis:

export default class MyBox extends React.Component {
  clickHandler(e) {
    console.log('click callback', e)
  }
  render() {
    return (
      <div className="box" onClick={this.clickHandler}>The text content</div>)}}Copy the code

Event registration

Starting with the setInitialDOMProperties method, which is used to traverse the props object of a ReactNode, Give the actual DOM object that you want to render a series of properties, such as style, class, and autoFocus, as well as the innerHTML and event handling. In this example, the props object for the.box element is as follows:

This method has a case for handling events:

// react-dom/src/client/ReactDOMComponent.js
else if (registrationNameModules.hasOwnProperty(propKey)) {
  if(nextProp ! =null) {
    if (true && typeofnextProp ! = ='function') {
      warnForInvalidEventListener(propKey, nextProp);
    }
    // Props for handling event typesensureListeningTo(rootContainerElement, propKey); }}Copy the code

RegistrationNameModules contains a host of properties related to React events:

The onClick props in this example obviously fits, so you can execute the ensureListeningTo method:

// react-dom/src/client/ReactDOMComponent.js
function ensureListeningTo(rootContainerElement, registrationName) {
  var isDocumentOrFragment = rootContainerElement.nodeType === DOCUMENT_NODE || rootContainerElement.nodeType === DOCUMENT_FRAGMENT_NODE;
  var doc = isDocumentOrFragment ? rootContainerElement : rootContainerElement.ownerDocument;
  listenTo(registrationName, doc);
}
Copy the code

In this method, we first check whether the rootContainerElement is a Document or Fragment. In this example, we passed the.box div, which is obviously not the case. So doc this variable was assigned to rootContainerElement ownerDocument, this thing is actually the box where the element of the document, the document to the following listenTo, event delegation is here to do, All events will eventually be delegated to either document or fragment, in most cases document, and the registrationName is the event name onClick

ListenTo (listenTo); listenTo (listenTo); listenTo (listenTo); listenTo (listenTo); listenTo (listenTo)

// react-dom/src/events/ReactBrowserEventEmitter.js
var dependencies = registrationNameDependencies[registrationName];
Copy the code

RegistrationNameDependencies onClick registrationName is coming, and variable is a storage for the React event name and native browser event name corresponds to a Map, Through the map to get the corresponding browser native event name, registrationNameDependencies structure is as follows:

React does some cross-browser compatibility with event names, such as blur, change, Click focus, and other browser-native events

Next, iterate through the Dependencies array to the following cases:

// react-dom/src/events/ReactBrowserEventEmitter.js
switch (dependency) {
  // Omit some code
  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.
    varisMediaEvent = mediaEventTypes.indexOf(dependency) ! = =- 1;
    if(! isMediaEvent) { trapBubbledEvent(dependency, mountAt); }break;
}
Copy the code

In addition to the scroll Focus blur cancel close method to trapCapturedEvent method, invalid Submit reset method does not handle the rest of the event types default, After executing the trapBubbledEvent method, the only difference between trapCapturedEvent and trapBubbledEvent is that for the final composite event, the former registers an event listener for the capture phase, while the latter registers an event listener for the bubble phase

Since most synthetic event agents register bubbling listeners, that is, delegate to document to register bubbling listeners, even if you declare a React capture event, such as onClickCapture, This event will also respond later than the capture and bubbling events of the native event. In fact, all native events (whether bubbling or capturing) will respond earlier than the React SyntheticEvent. Calling e.topPropagation () on native events will block the response to syntheticEvents because they will never reach the Document delegate layer

TrapBubbledEvent is used the most, and this example will execute this method as well, so go with it:

// react-dom/src/events/EventListener.js
// For this example, topLevelType is click and Element is document
function trapBubbledEvent(topLevelType, element) {
  if(! element) {return null;
  }
  var dispatch = isInteractiveTopLevelEventType(topLevelType) ? dispatchInteractiveEvent : dispatchEvent;

  addEventBubbleListener(element, getRawEventName(topLevelType),
  // Check if interactive and wrap in interactiveUpdates
  dispatch.bind(null, topLevelType));
}
Copy the code

The addEventBubbleListener method takes three arguments. In this case, the first argument, Element, is actually the document element, getRawEventName(topLevelType) is the click event, Dispatch is dispatchInteractiveEvent, and dispatchInteractiveEvent actually ends up executing the dispatchEvent method, just doing something extra before executing it, We don’t have to worry about that, but we can assume that they’re the same thing

Take a look at the addEventBubbleListener method:

// react-dom/src/events/EventListener.js
export function addEventBubbleListener(element: Document | Element, eventType: string, listener: Function,) :void {
  element.addEventListener(eventType, listener, false);
}
Copy the code

This method is very simple. It registers a bubble event to the document with an addEventListener, whose callback is dispatch.bind(null, topLevelType).

The flow chart is as follows:

Dispatching events

Since all events are delegated to document, when the event is emitted, there must be an event distribution process to figure out which element triggered the event and execute the corresponding callback function. Note that since the element itself did not register any events, but instead delegated to Document, So the event that will be triggered is a composite event that comes with React, not a browser-native event, but requires a distribution process

As mentioned earlier in event registration, the dispatchEvent method is triggered by the corresponding callback function for events registered on the document, which goes to this method:

// react-dom/src/events/ReactDOMEventListener.js
const nativeEventTarget = getEventTarget(nativeEvent);
let targetInst = getClosestInstanceFromNode(nativeEventTarget);
Copy the code

First find event triggers the DOM and React Component, looking for real DOM is better, directly take event callback event parameters of target | srcElement | window. The nativeEventTarget object is then attached to a property that starts with __reactInternalInstance, which is internalInstanceKey, The value is the React Component corresponding to the current React instance

Then read on:

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

BatchedUpdates, which literally means batch update, actually put the current event into the batch queue, where handleTopLevel is the core of the event distribution:

// react-dom/src/events/ReactDOMEventListener.js
let targetInst = bookKeeping.targetInst;

// Loop through the hierarchy, in case there's any nested components.
// It's important that we build the array of ancestors before calling any
// event handlers, because event handlers can modify the DOM, leading to
// inconsistencies with ReactMount's node cache. See #1105.
let ancestor = targetInst;
do {
  if(! ancestor) { bookKeeping.ancestors.push(ancestor);break;
  }
  const root = findRootContainerNode(ancestor);
  if(! root) {break;
  }
  bookKeeping.ancestors.push(ancestor);
  ancestor = getClosestInstanceFromNode(root);
} while (ancestor);
Copy the code

In this case, I initially thought that I would start from the current node and iterate through all the parent nodes, and then save this node link, but it turns out that this is not the case

function findRootContainerNode(inst) {
  // TODO: It may be a good idea to cache this to prevent unnecessary DOM
  // traversal, but caching is difficult to do correctly without using a
  // mutation observer to listen for all DOM changes.
  while (inst.return) {
    inst = inst.return;
  }
  if(inst.tag ! == HostRoot) {// This can happen if we're in a detached tree.
    return null;
  }
  return inst.stateNode.containerInfo;
}
Copy the code

FindRootContainerNode follows the parent element all the way to the root node, which is the usual

node

How did you find it? Each React Node has a return property that refers to the parent Node of the current Node. The root Node does not have a parent Node, so there is no return and the Node stops when it finds the root Node. Return the root node.

Generally speaking, for the same React application, the root node is fixed, so you can actually cache it. You don’t have to go up and look for the root node every time, but the function of going up and looking for the root node isn’t just to find the root node. If you can’t find the root node from the current node, it’s a problem. For example, the node has been removed from the React Tree. So there’s no need to do anything to this node later, right

This reason has been clearly written in the notes, which is the problem solved by issue #1105

As for why do… While/React/root/root/root/root/root/root/root While loop, but there are cases where there is more than one React application in an application

function getParent(inst) {
  do {
    inst = inst.return;
    // TODO: If this is a HostRoot we might want to bail out.
    // That is depending on if we want nested subtrees (layers) to bubble
    // events to their parent. We could also go through parentNode on the
    // host node but that wouldn't work for React Native and doesn't let us
    // do the portal feature.
  } while(inst && inst.tag ! == HostComponent);if (inst) {
    return inst;
  }
  return null;
}
Copy the code

Such as in a React instance application, I joined a React again application instance, and then the inner React on the application of a node with an event, the event at the time of execution, such as bubbling event, the event will start from the current node upwards through the parent node, in turn until the root node, The root node is the HostComponent, so it ends at the root node, that is, at the inner root node, but theoretically it should end at the outer root node, so we use the inner root node again as the event trigger source, continue to look up, connect the React application, and bubble until it reaches the outer root node

Moving on:

// react-dom/src/events/ReactDOMEventListener.js
for (let i = 0; i < bookKeeping.ancestors.length; i++) {
  targetInst = bookKeeping.ancestors[i];
  runExtractedEventsInBatch(
    bookKeeping.topLevelType,
    targetInst,
    bookKeeping.nativeEvent,
    getEventTarget(bookKeeping.nativeEvent),
  );
}
Copy the code

RunExtractedEventsInBatch method, is the entrance to the event to perform

Event to perform

RunExtractedEventsInBatch this method again call two methods: ExtractEvents, runEventsInBatch, the extractEvents are used to construct composite events, and runEventsInBatch is used to process composite events constructed by the batch extractEvents

Tectonic synthesis event

Find the appropriate composite eventplugin

Look at the extractEvents

// packages/events/EventPluginHub.js
let events = null;
for (let i = 0; i < plugins.length; i++) {
  // Not every plugin in the ordering may be loaded at runtime.
  const possiblePlugin: PluginModule<AnyNativeEvent> = plugins[i];
  if (possiblePlugin) {
    const extractedEvents = possiblePlugin.extractEvents(
      topLevelType,
      targetInst,
      nativeEvent,
      nativeEventTarget,
    );
    if(extractedEvents) { events = accumulateInto(events, extractedEvents); }}}Copy the code

The plugins are an array of synthesized plugins for all events. There are 5 types of plugins (v15.x has 7 types). These plugins are located in the react-dom/ SRC /events folder as separate files. Names ending in EventPlugin are those that were injected during the EventPluginHub initialization phase:

// react-dom/src/client/ReactDOMClientInjection.js
EventPluginHub.injection.injectEventPluginsByName({
  SimpleEventPlugin: SimpleEventPlugin,
  EnterLeaveEventPlugin: EnterLeaveEventPlugin,
  ChangeEventPlugin: ChangeEventPlugin,
  SelectEventPlugin: SelectEventPlugin,
  BeforeInputEventPlugin: BeforeInputEventPlugin,
});
Copy the code

The extractEvents method uses a for loop to execute all the plugins once, but it is not necessary to find the appropriate plugin and then break it. For example, for the click event in this example, the extractEvents method uses a for loop to execute all the plugins once. The right plugin is SimpleEventPlugin. The other plugins are just a waste of time, even after going through them once. Because the extractedEvents obtained after the execution of other plugins do not meet the condition of if (extractedEvents), the variable events cannot be assigned or overwritten. Of course, this code may also have other hidden functions

PossiblePlugin extractEvents this sentence is call the plugin the structure of the synthetic method of events, other plugin is not analyzed, for this example SimpleEventPlugin extractEvents look at it:

// react-dom/src/events/SimpleEventPlugin.js
const dispatchConfig = topLevelEventsToDispatchConfig[topLevelType];
if(! dispatchConfig) {return null;
}
Copy the code

First, look at the topLevelEventsToDispatchConfig there any topLevelType this property in the object, as long as there is, it means that the current events can use SimpleEventPlugin structural synthesis, for this sample, TopLevelType is click and topLevelEventsToDispatchConfig structure is as follows:

These attributes are some of the common event name, apparently the click is topLevelEventsToDispatchConfig an attribute name, qualified, can continue to perform the below is followed by a switch… The judgment statement for case, for this example, breaks at the following case:

// react-dom/src/events/SimpleEventPlugin.js
case TOP_CLICK:
  // Omitted some code
  EventConstructor = SyntheticMouseEvent;
  break;
Copy the code

SyntheticMouseEvent can be thought of as a concrete child plugin of SimpleEventPlugin, which adds a layer of plugin to the larger concept of SimpleEventPlugin. In addition to SyntheticMouseEvent, there are SyntheticWheelEvent, SyntheticClipboardEvent, SyntheticTouchEvent and so on

Gets an object from the synthetic event object pool

After setting up the specific EventConstructor, proceed:

// react-dom/src/events/SimpleEventPlugin.js
const event = EventConstructor.getPooled(
  dispatchConfig,
  targetInst,
  nativeEvent,
  nativeEventTarget,
);
accumulateTwoPhaseDispatches(event);
return event;
Copy the code

GetPooled fetches synthesized events from the Event object pool. This operation is one of the highlights of React. Caching all events in the React object pool greatly reduces the creation and destruction time of objects and improves performance

GetPooled is a method on EventConstructor that is attached when EventConstructor is initialized, but in the end, this method is on the SyntheticEvent object, and the flow diagram looks like this:

GetPooled is getPooledEvent, which is initialized during SyntheticEvent initialization:

// packages/events/SyntheticEvent.js
addEventPoolingTo(SyntheticEvent);
// Omit some code
function addEventPoolingTo(EventConstructor) {
  EventConstructor.eventPool = [];
  EventConstructor.getPooled = getPooledEvent;
  EventConstructor.release = releasePooledEvent;
}
Copy the code

So look at getPooledEvent:

// packages/events/SyntheticEvent.js
function getPooledEvent(dispatchConfig, targetInst, nativeEvent, nativeInst) {
  const EventConstructor = this;
  if (EventConstructor.eventPool.length) {
    const instance = EventConstructor.eventPool.pop();
    EventConstructor.call(
      instance,
      dispatchConfig,
      targetInst,
      nativeEvent,
      nativeInst,
    );
    return instance;
  }
  return new EventConstructor(
    dispatchConfig,
    targetInst,
    nativeEvent,
    nativeInst,
  );
}
Copy the code

For the first time when the triggering event (in this case, is the click event), EventConstructor. EventPool. The length is 0, because this time is the first event trigger, reference object pool has no corresponding synthetic events, so you need to initialize, subsequent triggering event again, Need not new, but the above logic, directly from the object in the pool, through EventConstructor. EventPool. Pop (); Gets the composite object instance

So let’s take a look at the initialization process. It does new EventConstructor. As mentioned earlier, this thing can be viewed as a subclass of SyntheticEvent, or something that’s extended from SyntheticEvent. We actually use a extend method:

const SyntheticMouseEvent = SyntheticUIEvent.extend({
  screenX: null.screenY: null.clientX: null.clientY: null.pageX: null.pageY: null.// Omit some code
})
Copy the code

First of all, SyntheticMouseEvent, a synthetic event, has its own properties. These properties are not that different from the event properties of the browser’s native event callback object. They all have some description of the current event, even the same property name. React generates SyntheticMouseEvent, or SyntheticEvent, as opposed to the browser’s native event callback object. React generates SyntheticEvent, or SyntheticEvent. Make the description attribute attached to it fully W3C compliant, so it has cross-browser compatibility at the event level, same interface as native browser events, stopPropagation() and preventDefault()

For the click event callback method in this example:

clickHandler(e) {
  console.log('click callback', e)
}
Copy the code

The e is actually the event of the composite event rather than the native event of the browser. Therefore, the developer does not need to consider browser compatibility and only needs to set the value according to the W3C specification. If the native event object needs to be accessed, it can be obtained through E.ativeEvent

The SyntheticUIEvent thing is basically just adding some extra properties to the SyntheticMouseEvent, so don’t worry about that, Then the SyntheticMouseEvent. The extend is expanded from SyntheticEvent (extend), so will the new SyntheticEvent

Take a look at the extend method:

// packages/events/SyntheticEvent.js
SyntheticEvent.extend = function(Interface) {
  const Super = this;

  // Original type inheritance
  const E = function() {};
  E.prototype = Super.prototype;
  const prototype = new E();
  // Constructor inheritance
  function Class() {
    return Super.apply(this.arguments);
  }
  Object.assign(prototype, Class.prototype);
  Class.prototype = prototype;
  Class.prototype.constructor = Class;

  Class.Interface = Object.assign({}, Super.Interface, Interface);
  Class.extend = Super.extend;
  addEventPoolingTo(Class);

  return Class;
};
Copy the code

React lets EventConstructor inherit from SyntheticEvent to get the properties and methods of the SyntheticEvent. As mentioned earlier, eventPool, getPooled, etc

Since the inheritance relationship exists, the new EventConstructor subclass naturally calls the new method of its SyntheticEvent parent class, which starts calling the constructor of the synthesized component and starts actually constructing the synthesized event by attaching parameters from the native browser event to the synthesized event. It includes clientX, screenY, timeStamp and other event properties, preventDefault, stopPropagation and other event methods, such as e.ativeEvent.

// packages/events/SyntheticEvent.js
this.nativeEvent = nativeEvent;
Copy the code

The mount properties are handled by React and have cross-browser capabilities. Similarly, the mount methods are different from the event methods of native browsers because the event is attached to the document, so some event methods are called. For example, e.topPropagation () is called to the document element, which is not the same element as it was intended to be. In order for the composite event to behave like the original event, additional processing is required for these methods

React encapsulates stopPropagation by adding a flag bit:

// packages/events/SyntheticEvent.js
stopPropagation: function() {
  const event = this.nativeEvent;
  if(! event) {return;
  }

  if (event.stopPropagation) {
    event.stopPropagation();
  } else if (typeofevent.cancelBubble ! = ='unknown') {
    // The ChangeEventPlugin registers a "propertychange" event for
    // IE. This event does not support bubbling or cancelling, and
    // any references to cancelBubble throw "Member not found". A
    // typeof check of "unknown" circumvents this issue (and is also
    // IE specific).
    event.cancelBubble = true;
  }

  this.isPropagationStopped = functionThatReturnsTrue;
}
Copy the code

The first step is to get the browser’s native event, and then call the corresponding stopPropagation method. Note that the event is generated by the event callback of the document element, not the event callback of the actual element. To prevent the event from propagating to the parent of document or Fragment, click on the event

// packages/events/SyntheticEvent.js
// This function returns true, and corresponding to this function is functionThatReturnsFalse, which returns false
function functionThatReturnsTrue() {
  return true;
}
Copy the code

Is the key to this. IsPropagationStopped = functionThatReturnsTrue; For bubbling events, when the event is triggered, the event call-back for each layer of elements is executed sequentially, but if isPropagationStopped is true on the synthetic event corresponding to the current element, The loop is broken, that is, the loop is not iterated further, and the composite events of all the parent elements of the current element will not be triggered. The result is the same as the result of the browser’s native event call e.topPropagation ()

Capturing events works the same way, except that they are traversed from parent to child

These event methods (stopPropagation, preventDefault, etc.) are generally called within the event callback function, which is executed later in the batch operation

var event = EventConstructor.getPooled(dispatchConfig, targetInst, nativeEvent, nativeEventTarget);
accumulateTwoPhaseDispatches(event);
Copy the code

Gets all the element instances and event callback functions associated with the currently triggered event

The above a lot of the code from the first sentence getPooled for entry into, mainly in order to get synthetic events after getting the synthesis of basic events, to the synthetic events for further processing, namely accumulateTwoPhaseDispatches this method to do, This method involves more processes, so draw a clearer picture:

The code and methods are trivial, but the goal is to save all of the event call-back functions that captured and bubbled on the element and its parent to the _dispatchListeners attribute of the event. And save the react instance of the current element and its parent (in the V16.x version, a FiberNode example) to the _dispatchInstances property of the Event

Once you have all the element instances associated with the event and the event callback function, you can batch process the synthesized event

The React event mechanism is complex and has a lot to say about it. This article is divided into two parts. For the rest of the analysis, see the article React Event mechanism.