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:

React takes over event delegation, which is a common browser event optimization strategy. It also eliminates browser differences and gives developers a cross-browser experience by using EventPluginHub to schedule events. Compose events and create and destroy them as a pool of objects. The structure below is a graphical representation of the event mechanism

  • The React event delegate mechanism is used to reduce the number of registered events on the page, reduce memory overhead, and optimize browser performance. In addition, the React event delegate mechanism is also used to better manage events. All events in React end up being delegated to the document top-level DOM
  • Since all events are delegated to document, there must be some sort of management mechanism, and all events are triggered and called back in a first-in, first-out queue
  • React has its own synthetic events, which are synthesized by the corresponding EventPlugin. Events of different types are synthesized by different plugins. For example, SimpleEvent Plugin, TapEvent Plugin, etc
  • To further improve the performance of events, an EventPluginHub is used to take care of the creation and destruction of composite event objects

# start

<button onClick={this.autoFocus}>Copy the code

This is how we normally write events bound in React. After JSX parsing, the Button is mounted as a component. OnClick is just a prop at this point. The ReactDOMComponent needs to handle props (_updateDOMProperties) when performing component loading (mountComponent) and updating (updateComponent) :

Event registration

ReactDOMComponent.Mixin = {
    mountComponent:function(){},
    _createOpenTagMarkupAndPutListeners:function() {},... NextProp is the current value of the property, where nextProp is the onclick event handler we bound to the component. // nextProp does not bind events to enqueuePutListener for air conditioner. If it is empty, the event binding is unbound. _updateDOMProperties:function(lastProps, nextProps, transaction){
         for (propKey in{// omit...for (propKey inNextProps) {// Check whether this is an event attributeif(registrationNameModules.hasOwnProperty(propKey)) { enqueuePutListener(this, propKey, nextProp, transaction); }}}} // This method is only implemented in the browser environment, The listenTo parameters are the event name, respectively'onclick'And the binding DOM of the proxy event. If fragement is the root node (specified in reactdom.render), if not document. ListenTo is used to bind the event to the document, and the following is handed over to the transaction to store the callback function for easy invocation. ListenTo in the ReactBrowserEventEmitter file is the source of event processing. This gets the document where the current component (in this case, the button) is locatedfunctionenqueuePutListener(inst, registrationName, listener, transaction) { ... var containerInfo = inst._hostContainerInfo; var isDocumentFragment = containerInfo._node && containerInfo._node.nodeType === DOC_FRAGMENT_TYPE; var doc = isDocumentFragment ? containerInfo._node : containerInfo._ownerDocument; listenTo(registrationName, doc); . }Copy the code

The focus of binding is the listenTo method here. See the source code (ReactBrowerEventEmitter)

ListenTo: listenTo: listenTo: listenTo: listenTo:function(registrationName, contentDocumentHandle) { var mountAt = contentDocumentHandle; Var isListening = getListeningForDocument(mountAt); // get topLevelEvent (topLevelEvent type) var dependencies = of registrationName EventPluginRegistry.registrationNameDependencies[registrationName];for (var i = 0; i < dependencies.length; i++) {
      var dependency = dependencies[i];
      if(! (isListening.hasOwnProperty(dependency) && isListening[dependency])) {if (dependency === 'topWheel') {... }else if (dependency === 'topScroll') {... }else if (dependency === 'topFocus' || dependency === 'topBlur') {... }else if(topEventMapping hasOwnProperty (dependency)) {/ / get topLevelEvent corresponding native browser events / / bubbling ReactBrowserEventEmitter.ReactEventListener.trapBubbledEvent(dependency, topEventMapping[dependency], mountAt); } isListening[dependency] =true; }}},Copy the code

For the same event, for example, click has two event names: onClick (triggered during the bubble phase) and onClickCapture (triggered during the capture phase). Both the bubble and capture are simulated by the React event. The events bound to the document are basically in the bubbling phase (with extra processing for Whell, Focus, scroll), and the click event binding is performed as shown below.

Final processing (in Listen and Capture of EventListener)

//eventType: eventType,target: document object, //callback: is fixed, always ReactEventListener's dispatch methodif (target.addEventListener) {
      target.addEventListener(eventType, callback, false);
      return {
        remove: function remove() {
          target.removeEventListener(eventType, callback, false); }}; }Copy the code

All events are bound to document

So events trigger ReactEventListener’s dispatch method

The callback is stored

If you’re wondering, all callbacks execute the ReactEventListener dispatch method, what’s going on with my callback? Don’t worry, read on:

functionenqueuePutListener(inst, registrationName, listener, transaction) { ... // Note here !!!!!!!!! Var doc = isDocumentFragment = isDocumentFragment; containerInfo._node : containerInfo._ownerDocument; ListenTo (registrationName, doc); // This code puts putListener into the callback sequence. When the component is mounted, the callback sequence will be executed. PutListener is also executed at that time. / / don't understand can look at this column of the top two articles about the transaction and transaction. The interpretation of mount mechanism getReactMountReady (). The enqueue (putListener, {inst: inst, registrationName: registrationName, listener: listener }); // Save the callbackfunction putListener() { var listenerToPut = this; EventPluginHub.putListener(listenerToPut.inst, listenerToPut.registrationName, listenerToPut.listener); }}Copy the code

This is the same code, the event binding that we talked about, mainly the listenTo method. PutListener is executed when the binding is complete. This method can be carried in ReactReconcileTransaction affairs close phase, specific to be managed by EventPluginHub

//
var listenerBank = {};
var getDictionaryKey = function(inst) {//inst is the instantiated object of the component. //_rootNodeID is the unique identifier of the componentreturn '. '+ inst._rootNodeID; } var EventPluginHub = {//inst for the instantiation object //registrationName for the event name // Listner for the callback function, which is this. AutoFocus putListener:function(inst, registrationName, listener) { ... var key = getDictionaryKey(inst); var bankForRegistrationName = listenerBank[registrationName] || (listenerBank[registrationName] = {}); bankForRegistrationName[key] = listener; . }}Copy the code

The EventPluginHub is instantiated only once per project. That is, callbacks to all project events are stored in a single listenerBank.

Is it a little dizzy, put on the flow chart, carefully recall

Events trigger

As we said when we registered events, all events are bound to documents. The callback is the dispatch method of ReactEventListener. Because of the bubbling mechanism, whichever DOM we click on ends up being responded to by the Document (because no other DOM listens for events at all). That is, dispatch is triggered

dispatchEvent: function(topLevelType, nativeEvent) {var nativeEventTarget = getEventTarget(nativeEvent); / / nativeEventTarget corresponding virtual DOM var targetInst = ReactDOMComponentTree. GetClosestInstanceFromNode (nativeEventTarget, ); . // Create an instance of bookKeeping For handleTopLevelImpl callback function relay event name and native / / event object is the object of three parameters are encapsulated into a var bookKeeping = getTopLevelCallbackBookKeeping (topLevelType, nativeEvent, targetInst, ); Try {// start a transactIon, Perform that executes the / / handleTopLevelImpl (bookKeeping) ReactGenericBatching. BatchedUpdates (handleTopLevelImpl, bookKeeping); } finally { releaseTopLevelCallbackBookKeeping(bookKeeping); }},Copy the code
functionHandleTopLevelImpl (bookKeeping) {// The actual DOM var nativeEventTarget = getEventTarget(bookKeeping. / / nativeEventTarget corresponding ReactElement var targetInst = ReactDOMComponentTree getClosestInstanceFromNode (nativeEventTarget);  //bookKeeping. Rooted saves the component. var ancestor = targetInst;do {
    bookKeeping.ancestors.push(ancestor);
    ancestor = ancestor && findParent(ancestor);
  } while (ancestor);

  for(var i = 0; i < bookKeeping.ancestors.length; i++) { targetInst = bookKeeping.ancestors[i]; / / the specific processing logic ReactEventListener. _handleTopLevel (bookKeeping. TopLevelType, targetInst, bookKeeping nativeEvent, getEventTarget(bookKeeping.nativeEvent)); }}Copy the code
// This is how the core handles handleTopLevel:function(topLevelType, targetInst, nativeEvent, NativeEventTarget) {/ / first encapsulation event event var. Events = EventPluginHub extractEvents (topLevelType targetInst, nativeEvent, nativeEventTarget); // Send the wrapped event runEventQueueInBatch(events); }Copy the code

Events encapsulated

The first is the extractEvents of EventPluginHub

extractEvents: function (topLevelType, targetInst, nativeEvent, nativeEventTarget) {
    var events;
    var plugins = EventPluginRegistry.plugins;
    for (var i = 0; i < plugins.length; i++) {
      // Not every plugin in the ordering may be loaded at runtime.
      var possiblePlugin = plugins[i];
      if(possiblePlugin) {/ / depends mostly on var extractedEvents = possiblePlugin extractEvents (topLevelType targetInst, nativeEvent, nativeEventTarget); . }}return events;
  },
Copy the code

Now look at the methods of the SimpleEventPlugin

extractEvents: function(topLevelType, targetInst, nativeEvent, nativeEventTarget) { ...... // Here is the encapsulation of the event, But we are not focused on var event = EventConstructor. GetPooled (dispatchConfig targetInst, nativeEvent, nativeEventTarget); . / / focus on this side EventPropagators accumulateTwoPhaseDispatches (event);return event;
}
Copy the code

Then the various references in the method, jump and jump and turn, and we come to the traverseTwoPhase method in redo DomTraversal

/ / inst is triggering event target ReactElement / / fn: EventPropagator accumulateDirectionalDispatches / / arg: It is a partially encapsulated event (partially, because we are processing the event now, so the encapsulation is completed).function traverseTwoPhase(inst, fn, arg) {
  var path = [];
  while(inst) {// Notice the path, which bubbles up in the form of ReactElement, // save the parent of the event in turn. // Get parent inst = inst._hostparent; } var i; // capture, sequence processingfor (i = path.length; i-- > 0;) {
    fn(path[i], 'captured', arg); } // bubblefor (i = 0; i < path.length; i++) {
    fn(path[i], 'bubbled', arg); }}Copy the code
// Determine whether the parent component stores such eventsfunctionAccumulateDirectionalDispatches (inst, phase, the event) {/ / get the callback var listener = listenerAtPhase (inst, event, phase);if(Listener) {// If there is a callback, the DOM containing the listener for that type of Event and the corresponding callback are saved into the Event. // This is an accumulateInto attribute. Assign // event._dispatchListeners = accumulateInto(event._dispatchListeners, listener); event._dispatchInstances = accumulateInto(event._dispatchInstances, inst); }}Copy the code

Inside the listenerAtPhase is the getListener function of EventPluginHub

getListener: function(inst, registrationName) {// Remember the listenerBank that saved the callback? var bankForRegistrationName = listenerBank[registrationName];if (shouldPreventMouseEvent(registrationName, inst._currentElement.type, inst._currentElement.props)) {
      returnnull; } var key = getDictionaryKey(inst); // Get the corresponding callbackreturn bankForRegistrationName && bankForRegistrationName[key];
  },
Copy the code

Dispatching events

RunEventQueueInBatch performs two operations

functionRunEventQueueInBatch (events) {/ / to join processEventQueue event event sequence EventPluginHub enqueueEvents (events); / / in the previous step to save good processEventQueue executed in sequence / / executeDispatchesAndRelease EventPluginHub. ProcessEventQueue (false);
}

  processEventQueue: function (simulated) {
    var processingEventQueue = eventQueue;
    eventQueue = null;
    if (simulated) {
      forEachAccumulated(processingEventQueue, executeDispatchesAndReleaseSimulated);
    } else{// focus here //forEachAccumulated is one way to think of itforEach package / / here is processingEventQueue saved event in turn perform executeDispatchesAndReleaseTopLevel (event)forEachAccumulated(processingEventQueue, executeDispatchesAndReleaseTopLevel); }},Copy the code

ExecuteDispatchesAndReleaseTopLevel (event) packaging and a variety of functions, the last work is

functionExecuteDispatchesInOrder (event, event) {// Var dispatchListeners = event._dispatchlisteners; // ReactElement array with eventType attribute var dispatchInstances = event._dispatchinstances; .if (Array.isArray(dispatchListeners)) {
    for (var i = 0; i < dispatchListeners.length; i++) {
      if (event.isPropagationStopped()) {
        break; } executeDispatch(event, simulated, dispatchListeners[i], dispatchInstances[i]); }}else if (dispatchListeners) {
    executeDispatch(event, simulated, dispatchListeners, dispatchInstances);
  }
  event._dispatchListeners = null;
  event._dispatchInstances = null;
}
Copy the code

The two attributes of the Event, dispatchListeners and dispatchInstances, can be used in the process of assembling nativeEvent. The code is simple; if there is a callback to handle the event, it handles it once. We’ll talk about the details later, but let’s see what’s going on here, okay

function executeDispatch(event, simulated, listener, inst) {
//typeIs the event type vartype = event.type || 'unknown-event'; / / this is the real trigger the DOM, is the example of the button event. The currentTarget. = EventPluginUtils getNodeFromInstance (inst);if (simulated) {
    ReactErrorUtils.invokeGuardedCallbackWithCatch(type, listener, event);
  } else{/ / here look here ReactErrorUtils. InvokeGuardedCallback (type, listener, event);
  }
  event.currentTarget = null;
}

Copy the code

Finally, the code is in ReactErrorUtil (for development purposes React emulates real browser events to get better DevTools integration. This code runs in development mode.)

Create a temporary DOM var fakeNode = document.createElement('react');
    ReactErrorUtils.invokeGuardedCallback = function(name, func, a) {// Bind the context of the callback function var boundFunc = func.bind(null, a); // Define the event type var evtType ='react-'+ name; / / bind event fakeNode. AddEventListener (evtType boundFunc,false); Var evt = document.createEvent('Event'); InitEvent (evtType, evtType, evtType)false.false); // Publish events -- the fakenode.dispatchEvent (EVT) callback is executed; / / removed from the event listeners fakeNode. RemoveEventListener (evtType boundFunc,false);
    };

Copy the code

The flow chart