I study the source code is 0.8.0 version, may be a bit different from the latest version of the event system.

What is a synthetic event system?

First, the term Synthetic Event is translated from “Synthetic Event”. In the React documentation and source code, the term narrowly refers to Synthetic Event objects, which are ordinary javascript objects. Here, we’re talking about a React’s Event System made up of many synthetic Event objects of different types of events. In my understanding, composite events are relative to the browser’s native event system. Synthetic event system is essentially to follow the W3C specifications, the browser implemented the event system again, and erase the implementation differences of various browsers, so that developers use the experience is consistent.

Before we begin to understand what a synthesized event system is, let’s take a look at my translation of react’s synthesized event object. From this document, we can draw the following conclusions about the similarities and differences between synthetic event systems and native event systems:

The same

  • inevent target.current event target.event object.event phasepropagation pathAnd so on the core concept of the definition is consistent.
  • The dispatch mechanism is consistent: the firing of an event causes an Event object to followpropagation pathOn the transmission. In other words, the samepropagation pathFor each event Listener onevent objectIt’s all the same.

In other words, the architecture and interface implemented by the two are the same. Both follow W3C standard specifications.

The difference between

  • Registration methods are inconsistent.
    1. Take DOM Level 1, which also registers event listeners with inline attributes. In the native event system, attribute names are all lowercase, such as “onclick” and “onmousedown”. In the React synthetic event system, attribute names are written with small humps, such as: “OnClick”,”onMouseDown”.
    2. If JSX is considered markup language (because JSX will eventually be converted to plain JS code, so if), the native event system currently has a DOM Level of 1 (there is a DOM Level of 0, but it is not the actual standard, It refers to the three ways events are registered from DHTML initially supported by IE4 and Netscape Navigator 4.0 to DOM Level 3, but in react’s synthetic event system, only the inline attributes described above are registered.
    3. There are different ways to register captured events. In the native event system, we use the second Boolean parameter of DOM Level 3’s addEventListener() method to indicate whether or not to bind an EventListener in the capture phase. However, in the React composite event system, if you want to bind in the capture phase, you use a property name like “onClickCapture”.
  • In event listeners, this points differently. In a native event system, this in the event listener points to the Current Event target. In react’s synthetic event system, this points to the current component instance.
  • In event Listeners, the Event Object is different. In the React synthetic event system, the Event object we get is the wrapper of the native Event object. More specifically, the nativeEvent object is mounted on the composite event object as a key named nativeEvent. Or in other words, the synthesized event object is the parent set of the native event object.
  • Compared with native event objects, the React synthesized event system introduces pooling technology into synthesized event objects. The reason for doing so, in the official words, is that “These systems should generally use pooling to reduce the frequency of garbage collection.”

I mention the differences between the React composite event system and the native system because I think the question “What’s the reason for the difference?” This question is more pertinent to explore react’s synthetic event system. Because the source code is often complex, as vacant and marginal primitive forest in general, once we have no goal, it is easy to get lost in this primitive forest, and ultimately nothing.

The architecture of the composite event system

In the reacteventEmitter.js source code, the architecture diagram looks like this:

+------------+ . | DOM | . +---^--------+ . +-----------+ | + . +--------+|SimpleEvent| | | . | |Plugin | +---|--|------+ . v +-----------+ | | | | . +--------------+ +------------+ | | +-------------->|EventPluginHub| | Event  | | | . | | +-----------+ | Propagators| | ReactEvent | . | | |TapEvent | |------------| | Emitter | . | |<---+|Plugin | |other plugin| | | . | | +-----------+ | utilities | | +-----------.---------+ | +------------+ | | | . +----|---------+ +-----|------+ . | ^ +-----------+ | . | | |Enter/Leave| + . | +-------+|Plugin | +-------------+ . v +-----------+ | application | . +----------+ |-------------| . | callback | | | . | registry | | | . +----------+ +-------------+ . . React Core . General Purpose Event Plugin SystemCopy the code

From the official architecture diagram, we can see the following main roles:

  • DOM (here, it can be equivalent to the browser environment)
  • Application (code written by developers)
  • ReactEventEmitter (event emitter, used to bridge DOM and application)
  • EventPluginHub (hub for hosting various EventPlugins)
  • XxxEventPlugin (responsible for composing event objects of various types)
  • CallbackRegistry (the registry for callback, which is mainly responsible for storage, to find the Event Listenrer we wrote)

Let’s analyze the relationship between them.

Note that there is no relation chain from ReactEventEmitter to DOM in the original architecture diagram, which WAS added by myself.

The main relationship

  1. The relationship from the ReactEventEmitter to the DOM means that the ReactEventEmitter is responsible for delegating events to the top layer of the DOM. The general call stack corresponding to this relation chain is (the smaller the serial number, the earlier the call) :

    1. ReactMount.prepareEnvironmentForDOM()
    2. ReactEventEmitter.ensureListening()
    3. ReactEventEmitter.listenAtTopLevel()
    4. EventListenter.listen()
    5. Call the native addEventListener or attachEvent method on the Document object (called topLevel) for event listening.
  2. The “DOM -> ReactEventEmitter -> EventPluginHub -> CallbackRegistry” chain refers to when a user interacts with the DOM to trigger a native event, Because ReactEventEmitter registers listeners for events at the top level through createTopLevelCallback, ReactEventEmitter is the first to be notified. The ReactEventEmitter then informed EventPluginHub to find the event Plugin, to synthesize the event object needed for this event dispatch, and to execute the dispatch task. During the execution of the Dispatch task, the EventPluginHub needs to find the corresponding Event Listener (or event Callback) from the CallbackRegistry and invoke it. The general call stack corresponding to this chain is:

    1. The firing of the native event causes a call to the TopLevelCallback on the Document object.
    2. ReactEventEmitter.handleTopLevel()
    3. EventPluginHub.extractEvents()
    4. CallbackRegistry.getListener()
  3. The “Application -> ReactEventEmitter -> EventPluginHub -> CallbackRegistry” relationship exists in the Application Event Listener storage phase. ReactEventEmitter collects and stores event Listeners written by developers during the first mount of the React Component. In my opinion, only the “application -> CallbackRegistry” relationship is needed. I don’t know why the source code uses reference passing, around and around, the whole relationship chain extends so long. The general call stack corresponding to this chain is:

    1. ReactDOMComponent. _createOpenTagMarkup ().
    2. ReactEventEmitter.putListener()
    3. ReactEventEmitter. PutListener EventPluginHub reference. PutListener
    4. EventPluginHub. PutListener CallbackRegistry reference. PutListener
    5. CallbackRegistry. PutListener ()
  4. Relationship between xxxEventPlugin and EventPluginHub.

    Various “xxxEventPlugins” are injected into the EventPluginHub, in other words, Is “xxxEventPlugin” reference will be mounted in EventPluginHub. RegistrationNames object on each key. The specific data structure after injection looks like this:

    EventPluginHub.registrationNames = {
        onBlur: xxxEventPlugin,
        onBlurCapture: xxxEventPlugin,
        onChange: xxxEventPlugin,
        onChangeCapture: xxxEventPlugin,
        ......
    }
    Copy the code

    In the source code of V0.8.0, the eventPlugin mainly has the following:

    • SimpleEventPlugin
    • EnterLeaveEventPlugin
    • ChangeEventPlugin
    • CompositionEventPlugin
    • MobileSafariClickEventPlugin
    • SelectEventPlugin

    Since the “xxxEventPlugin” was injected into the EventPluginHub, where was it injected? A: This is done during the react. Js initialization phase, before the React root component is initially mounted. Reactdefaultinjection.js:

 /**
   * Some important event plugins included by default (without having to require
   * them).
   */
  EventPluginHub.injection.injectEventPluginsByName({
    SimpleEventPlugin: SimpleEventPlugin,
    EnterLeaveEventPlugin: EnterLeaveEventPlugin,
    ChangeEventPlugin: ChangeEventPlugin,
    CompositionEventPlugin: CompositionEventPlugin,
    MobileSafariClickEventPlugin: MobileSafariClickEventPlugin,
    SelectEventPlugin: SelectEventPlugin
  });
Copy the code

From the concept of collection, the relationship between EventPluginHub and eventPlugin is one-to-many. All EventPlugins are injected into the EventPluginHub.

  1. Relationship between xxxEventPlugin and SyntheticxxxEvent

    When composing an event object, EventPlugin needs to call the constructors of different synthetic events for different types of events. In other words, “xxxEventPlugin” and “SyntheticxxxEvent” form a one-to-many relationship. Let’s take the extractEvents method of SimpleEventPlugin as an example:

/**
   * @param {string} topLevelType Record from `EventConstants`.
   * @param {DOMEventTarget} topLevelTarget The listening component root node.
   * @param {string} topLevelTargetID ID of `topLevelTarget`.
   * @param {object} nativeEvent Native browser event.
   * @return {*} An accumulation of synthetic events.
   * @see {EventPluginHub.extractEvents}
   */
  extractEvents: function(
      topLevelType,
      topLevelTarget,
      topLevelTargetID,
      nativeEvent) {
    var dispatchConfig = topLevelEventsToDispatchConfig[topLevelType];
    if(! dispatchConfig) {return null;
    }
    var EventConstructor;
    switch(topLevelType) {
      case topLevelTypes.topInput:
      case topLevelTypes.topSubmit:
        // HTML Events
        // @see http://www.w3.org/TR/html5/index.html#events-0
        EventConstructor = SyntheticEvent;
        break;
      case topLevelTypes.topKeyDown:
      case topLevelTypes.topKeyPress:
      case topLevelTypes.topKeyUp:
        EventConstructor = SyntheticKeyboardEvent;
        break;
      case topLevelTypes.topBlur:
      case topLevelTypes.topFocus:
        EventConstructor = SyntheticFocusEvent;
        break;
      case topLevelTypes.topClick:
        // Firefox creates a click event on right mouse clicks. This removes the
        // unwanted click events.
        if (nativeEvent.button === 2) {
          return null;
        }
        /* falls through */
      case topLevelTypes.topContextMenu:
      case topLevelTypes.topDoubleClick:
      case topLevelTypes.topDrag:
      case topLevelTypes.topDragEnd:
      case topLevelTypes.topDragEnter:
      case topLevelTypes.topDragExit:
      case topLevelTypes.topDragLeave:
      case topLevelTypes.topDragOver:
      case topLevelTypes.topDragStart:
      case topLevelTypes.topDrop:
      case topLevelTypes.topMouseDown:
      case topLevelTypes.topMouseMove:
      case topLevelTypes.topMouseUp:
        EventConstructor = SyntheticMouseEvent;
        break;
      case topLevelTypes.topTouchCancel:
      case topLevelTypes.topTouchEnd:
      case topLevelTypes.topTouchMove:
      case topLevelTypes.topTouchStart:
        EventConstructor = SyntheticTouchEvent;
        break;
      case topLevelTypes.topScroll:
        EventConstructor = SyntheticUIEvent;
        break;
      case topLevelTypes.topWheel:
        EventConstructor = SyntheticWheelEvent;
        break;
      case topLevelTypes.topCopy:
      case topLevelTypes.topCut:
      case topLevelTypes.topPaste:
        EventConstructor = SyntheticClipboardEvent;
        break;
    }
    ("production"! == process.env.NODE_ENV ? invariant( EventConstructor,'SimpleEventPlugin: Unhandled event type, `%s`.',
      topLevelType
    ) : invariant(EventConstructor));
    var event = EventConstructor.getPooled(
      dispatchConfig,
      topLevelTargetID,
      nativeEvent
    );
    EventPropagators.accumulateTwoPhaseDispatches(event);
    return event;
  }
Copy the code

As you can see from the source code above, the extractEvents method uses different “SyntheticxxxEvent” constructors to construct synthesized event objects, depending on the event type. The SimpleEventPlugin uses the following constructors:

  • SyntheticEvent
  • SyntheticKeyboardEvent
  • SyntheticFocusEvent
  • SyntheticMouseEvent
  • SyntheticTouchEvent
  • SyntheticUIEvent
  • SyntheticWheelEvent
  • SyntheticClipboardEvent

The relationship between EventPluginHub, eventPlugin and SyntheticEvent is as follows:

Before analyzing each stage, let me give you a preview. Preview several data structures and two relationships.

The data structure

  • EventQueue (queue, implemented in javascript arrays).
  • SyntheticEvent Instance (object)
  • ListenerBank (object), like:
   listenerBank = {
      onClick: {
           '[0]. [1]': listener // Listener is the event callback we mount in JSX}, onClickCapture: {'[0]. [1]': listener
       }
   }
Copy the code

Relations between the two kinds of

  • EventPluginHub and various EventPlugins are interfaces and implementations.
  • Syntheticevents are inherited from various SyntheticxxxEvents.

Four stages

As I mentioned above, reading source code must have a focused goal. An event Listener registered in the React Component is called after the react Component is registered. After research, we can divide the life cycle of event Listener into four stages:

  1. Preparation stage

    1.1. Early injection of various EventPlugins (dependency injection)

    1.2. Listen for all supported events on document in advance (event delegate)

  2. Storage stage

    2.1. Find the collection entrance

    2.2. Store Application Event Listeners

  3. Call stage

    3.1. Find eventPlugin according to eventType

    3.2. Build an eventQueue(consisting of events that build different types of events)

    Step 1: Construct an instance of a SyntheticEvent;

    Step 2: Add various enhanced properties and compatibility properties to the SyntheticEvent instance to smooth out differences across browsers;

    Step 3: Fetch the Application Event Listener along the propagation path of the capture stage and the propagation path of the bubble stage respectively, and push it into the queue storing the Listener in the sequence of capture first and bubble later. Event._dispatchlisteners.

    3.3. Loop eventQueue, dispatch each SyntheticEvent in turn

  4. Finishing touches

    An eventQueue is garbage collected to release the SyntheticEvent instance back into the pooling pool.

Preparation stage

Two things were done in the preparation phase:

  • Dependency injection
  • Event Delegation

Dependency injection

Because react developers had long considered cross-platform development of React, they began separating the core code of React from platform-specific code. The dependency injection mode adopted in the early stage and the subcontracting mode adopted in the later stage are the main means of cross-platform architecture. In the React synthetic event system, the implementation of EventPluginHub is organized by using this dependency injection pattern. At the react.js entry, we inject the EventPluginHub dependency (see the beginning of reactDefaultinjection.js) :

function inject() {
  ReactEventEmitter.TopLevelCallbackCreator = ReactEventTopLevelCallback;
  /**
   * Inject module forresolving DOM hierarchy and plugin ordering. */ EventPluginHub.injection.injectEventPluginOrder(DefaultEventPluginOrder); EventPluginHub.injection.injectInstanceHandle(ReactInstanceHandles); /** * Some important event plugins included by default (without having to require * them). */ EventPluginHub.injection.injectEventPluginsByName({ SimpleEventPlugin: SimpleEventPlugin, EnterLeaveEventPlugin: EnterLeaveEventPlugin, ChangeEventPlugin: ChangeEventPlugin, CompositionEventPlugin: CompositionEventPlugin, MobileSafariClickEventPlugin: MobileSafariClickEventPlugin, SelectEventPlugin: SelectEventPlugin }); / /... other code here }Copy the code

From the code, we can see that we have injected EventPluginOrder, InstanceHandle, and all the EventPlugins needed by the platform (Web) into the EventPluginHub. I won’t repeat the explanation of what the eventPlugin is for, as mentioned above. We’ll talk about the remaining two: EventPluginOrder and InstanceHandle.

EventPluginOrder

Order of event plugins. More specifically, it refers to “the loading order of event plug-ins.” What is the order in which this is injected? See source DefaultEventPluginOrder. Js:

/**
 * Module that is injectable into `EventPluginHub`, that specifies a
 * deterministic ordering of `EventPlugin`s. A convenient way to reason about
 * plugins, without having to package every one of them. This is better than
 * having plugins be ordered in the same order that they are injected because
 * that ordering would be influenced by the packaging order.
 * `ResponderEventPlugin` must occur before `SimpleEventPlugin` so that
 * preventing default on events is convenient in `SimpleEventPlugin` handlers.
 */
var DefaultEventPluginOrder = [
  keyOf({ResponderEventPlugin: null}),
  keyOf({SimpleEventPlugin: null}),
  keyOf({TapEventPlugin: null}),
  keyOf({EnterLeaveEventPlugin: null}),
  keyOf({ChangeEventPlugin: null}),
  keyOf({SelectEventPlugin: null}),
  keyOf({CompositionEventPlugin: null}),
  keyOf({AnalyticsEventPlugin: null}),
  keyOf({MobileSafariClickEventPlugin: null})
];
Copy the code

To convert, the final value of DefaultEventPluginOrder looks like this:

var DefaultEventPluginOrder = [
  'ResponderEventPlugin'.'SimpleEventPlugin'.'TapEventPlugin'.'EnterLeaveEventPlugin'.'ChangeEventPlugin'.'SelectEventPlugin'.'CompositionEventPlugin'.'AnalyticsEventPlugin'.'MobileSafariClickEventPlugin'
];
Copy the code

That is, we need the eventPlugin to load and execute in the order described above. Why do you need to specify that EventPlugins are loaded and executed in a certain order? It is not difficult to find the answer to the problem from the source comments. Some plugins need to be loaded and executed before others. For example, the ResponderEventPlugin must be loaded before SimpleEventPlugin is loaded, otherwise the event ListEnter that SimpleEventPlugin handles cannot prevent the default behavior of the event. There is no guarantee that the packaging order will be 100% consistent, so the idea of manually importing each plugin sequentially and injecting each plugin sequentially is not reliable either. In contrast, it is better to explicitly declare the loading order of a plugin and then manually call the publishRegistrationName method to load the plugin.

From the source code given above:

EventPluginHub.injection.injectEventPluginsByName({
    SimpleEventPlugin: SimpleEventPlugin,
    EnterLeaveEventPlugin: EnterLeaveEventPlugin,
    ChangeEventPlugin: ChangeEventPlugin,
    CompositionEventPlugin: CompositionEventPlugin,
    MobileSafariClickEventPlugin: MobileSafariClickEventPlugin,
    SelectEventPlugin: SelectEventPlugin
  });
Copy the code

We can see that we use SimpleEventPlugin, EnterLeaveEventPlugin, ChangeEventPlugin, CompositionEventPlugin, MobileSafariClickEventPlugin and SelectEventPlugin these six plugin. And they are loaded in the order we gave above:

var DefaultEventPluginOrder = [
  'ResponderEventPlugin'.'SimpleEventPlugin'.'TapEventPlugin'.'EnterLeaveEventPlugin'.'ChangeEventPlugin'.'SelectEventPlugin'.'CompositionEventPlugin'.'AnalyticsEventPlugin'.'MobileSafariClickEventPlugin'
];
Copy the code
InstanceHandle

InstanceHandle is an utils module. It mainly contains utility functions for handling the react Instance aspect requirements. For example: createReactRootID, getReactRootIDFromNodeID, traverseTwoPhase, etc. Among them, traverseTwoPhase is most closely related to react’s synthetic event system. It will be used in the third stage we mentioned above. It is responsible for, given a reactID, finding and collecting event Listeners registered to the current event along the capture path and bubble path from the node to which the reactID corresponds, and enqueuing them in the correct order. The details of this part will be elaborated in stage 3.

In javascript, the essence of dependency injection is passing by reference. Therefore, we can say that JS dependency injection is implicit reference passing. If you look at the internal code of EventPluginHub, you can see that there is a lot of reference passing going on. The EventPluginHub module, which acts like a hand-in-hand, doesn’t really do much of anything, handing over most of its work to CallbackRegistry and EventPluginRegistry. This scene reminds me of a beautiful and sad “story” for Chinese programmers. In this story, Bob, like EventPluginHub, won by lying down without doing much.

In the first phase, in addition to EventPluginHub, ReactEventEmitter was also injected.

ReactEventEmitter.TopLevelCallbackCreator = ReactEventTopLevelCallback;
Copy the code

Injected ReactEventTopLevelCallback methods are used to create the binding on the top level event listener. We’ll elaborate on this in the event delegate section below.

Event delegation

The event delegate pattern is an old friend of ours, back in the days of jQuery. The core elements of the event delegation principle are the “event bubbling” mechanism of native events and the “event target”. The overall process of event delegation is as follows:

  1. Event listening for all types of events in the ancestor hierarchy of the element requiring event listening.
  2. Users register event Listeners on non-native elements (such as JQ objects, react Elements).
  3. The class library is responsible for collecting and storing this correspondence in the event listening registry.
  4. When a native event is triggered, the pre-registered event callback is executed. The process of executing the event callback is to search for the corresponding event listener in the event listener registry according to the event target and invoke them in the correct order.

At this point, it’s almost clear what “delegation” means. If “calling an event Listener” is something that needs to be done, instead of doing it ourselves (listening directly on the native DOM element and waiting for the browser to call our event Listener directly), we now “delegate” this to the ancestor of the native DOM element. Let it indirectly call our Event ListEnter when its event callback is called by the browser.

You might ask, “Why does the ancestor of the native DOM element have its event callback?” .

A: “Of course we need to manually do event listening in advance (referring to libraries like jQuery and React).

You might ask, “When a user clicks on a native DOM element, why does the event callback for its ancestor element execute?” .

Answer: “Because of the mechanism of” event bubbling “;

You might also ask, “All elements delegate event listeners to the same ancestor element, so how does that ancestor element know which event Listeners to call when an event is triggered?” .

Answer: According to the target attribute of the native Event object, we can first determine the path of the event propagation, and then collect the event Listener of all the binding elements on the path.

Whether in jQuery or React, the event delegate process is similar to the above mentioned. What we are really talking about here is the first step of the four processes. React listen at top level.

Source code is used in the “top level” this term, a source code review down, it actually refers to the “Document object”. Here is the source code (in reactmount.js) :

prepareEnvironmentForDOM: function(container) {
    ("production"! == process.env.NODE_ENV ? invariant( container && ( container.nodeType === ELEMENT_NODE_TYPE || container.nodeType === DOC_NODE_TYPE ),'prepareEnvironmentForDOM(...) : Target container is not a DOM element.') : invariant(container && ( container.nodeType === ELEMENT_NODE_TYPE || container.nodeType === DOC_NODE_TYPE ))); Document.documentelement = container. NodeType === ELEMENT_NODE_TYPE? container.ownerDocument : container; ReactEventEmitter.ensureListening(ReactMount.useTouchEvents, doc); }Copy the code

As noted in the comments above, the value of the doc variable is ultimately a Document object. If you go back down the call stack:

You’ll notice that our doc is passed to a method called Listen:

At this point, we also see the familiar native method “addEventListener”, we can also determine that the so-called “top level” is the Document object.

Ok, now that we’ve determined that “top level” is the Document object. So the next step is to explore how to listen at.

I’m not going to keep you guessing. React’s “Listen at” is an enumeration of all current browser events on a Document object. Source code (in ReacteventEmitter.js) :

listenAtTopLevel: function(touchNotMouse, contentDocument) {
    ("production"! == process.env.NODE_ENV ? invariant( ! contentDocument._isListening,'listenAtTopLevel(...) : Cannot setup top-level listener more than once.') : invariant(! contentDocument._isListening)); var topLevelTypes = EventConstants.topLevelTypes; var mountAt = contentDocument; registerScrollValueMonitoring();trapBubbledEvent(topLevelTypes.topMouseOver, 'mouseover', mountAt);
    trapBubbledEvent(topLevelTypes.topMouseDown, 'mousedown', mountAt);
    trapBubbledEvent(topLevelTypes.topMouseUp, 'mouseup', mountAt);
    trapBubbledEvent(topLevelTypes.topMouseMove, 'mousemove', mountAt);
    trapBubbledEvent(topLevelTypes.topMouseOut, 'mouseout', mountAt);
    trapBubbledEvent(topLevelTypes.topClick, 'click', mountAt);
    trapBubbledEvent(topLevelTypes.topDoubleClick, 'dblclick', mountAt);
    trapBubbledEvent(topLevelTypes.topContextMenu, 'contextmenu', mountAt);
    if (touchNotMouse) {
      trapBubbledEvent(topLevelTypes.topTouchStart, 'touchstart', mountAt);
      trapBubbledEvent(topLevelTypes.topTouchEnd, 'touchend', mountAt);
      trapBubbledEvent(topLevelTypes.topTouchMove, 'touchmove', mountAt);
      trapBubbledEvent(topLevelTypes.topTouchCancel, 'touchcancel', mountAt);
    }
    trapBubbledEvent(topLevelTypes.topKeyUp, 'keyup', mountAt);
    trapBubbledEvent(topLevelTypes.topKeyPress, 'keypress', mountAt);
    trapBubbledEvent(topLevelTypes.topKeyDown, 'keydown', mountAt);
    trapBubbledEvent(topLevelTypes.topInput, 'input', mountAt);
    trapBubbledEvent(topLevelTypes.topChange, 'change', mountAt);
    trapBubbledEvent(
      topLevelTypes.topSelectionChange,
      'selectionchange',
      mountAt
    );

    trapBubbledEvent(
      topLevelTypes.topCompositionEnd,
      'compositionend',
      mountAt
    );
    trapBubbledEvent(
      topLevelTypes.topCompositionStart,
      'compositionstart',
      mountAt
    );
    trapBubbledEvent(
      topLevelTypes.topCompositionUpdate,
      'compositionupdate',
      mountAt
    );

    if (isEventSupported('drag')) {
      trapBubbledEvent(topLevelTypes.topDrag, 'drag', mountAt);
      trapBubbledEvent(topLevelTypes.topDragEnd, 'dragend', mountAt);
      trapBubbledEvent(topLevelTypes.topDragEnter, 'dragenter', mountAt);
      trapBubbledEvent(topLevelTypes.topDragExit, 'dragexit', mountAt);
      trapBubbledEvent(topLevelTypes.topDragLeave, 'dragleave', mountAt);
      trapBubbledEvent(topLevelTypes.topDragOver, 'dragover', mountAt);
      trapBubbledEvent(topLevelTypes.topDragStart, 'dragstart', mountAt);
      trapBubbledEvent(topLevelTypes.topDrop, 'drop', mountAt);
    }

    if (isEventSupported('wheel')) {
      trapBubbledEvent(topLevelTypes.topWheel, 'wheel', mountAt);
    } else if (isEventSupported('mousewheel')) {
      trapBubbledEvent(topLevelTypes.topWheel, 'mousewheel', mountAt);
    } else {
      // Firefox needs to capture a different mouse scroll event.
      // @see http://www.quirksmode.org/dom/events/tests/scroll.html
      trapBubbledEvent(topLevelTypes.topWheel, 'DOMMouseScroll', mountAt);
    }

    // IE<9 does not support capturing so just trap the bubbled event there.
    if (isEventSupported('scroll'.true)) {
      trapCapturedEvent(topLevelTypes.topScroll, 'scroll', mountAt);
    } else {
      trapBubbledEvent(topLevelTypes.topScroll, 'scroll', window);
    }

    if (isEventSupported('focus'.true)) {
      trapCapturedEvent(topLevelTypes.topFocus, 'focus', mountAt);
      trapCapturedEvent(topLevelTypes.topBlur, 'blur', mountAt);
    } else if (isEventSupported('focusin')) {
      // IE has `focusin` and `focusout` events which bubble.
      // @see
      // http://www.quirksmode.org/blog/archives/2008/04/delegating_the.html
      trapBubbledEvent(topLevelTypes.topFocus, 'focusin', mountAt);
      trapBubbledEvent(topLevelTypes.topBlur, 'focusout', mountAt);
    }

    if (isEventSupported('copy')) {
      trapBubbledEvent(topLevelTypes.topCopy, 'copy', mountAt);
      trapBubbledEvent(topLevelTypes.topCut, 'cut', mountAt);
      trapBubbledEvent(topLevelTypes.topPaste, 'paste', mountAt); }}Copy the code

From the above code, we can see that React almost always listens for events that we’re familiar with, both in the bubble phase and (if supported) in the capture phase. The event name of topLevelType is written as a small hump with a “top” prefix. Toplevelcallback (); toplevelCallback (); toplevelCallback ();

After some exploration, the “Listen at top level” in the React composite event system is not as sophisticated as expected, and now it seems even awkward. Because the React composite event system uses the event delegate mode, and topLevelType is registered in the bubbling phase of the event, we can draw the following conclusions:

  1. The Event Listener bound to the React Element, either registered in the bubble phase or the capture phase, is executed later than the topLevelCallback registered in the bubble phase on the Documen object.
  2. The sequence of the event Listeners bound to the react Element events is the same as the sequence of the topLevelCallback bound to the events on your Documen object.

Here, I take the Click event as an example to verify conclusion 1:

// Type in the application codelog
handleClick=()=> {console.log('btn react click event')}}
handleClickCapture=()=> {console.log('btn react clickCapture event')}}
render() {
    return (
        <button 
            id="btn"OnClick ={this.handleclick} onClickCapture={this.handleclickcapture} > </button>)log
 createTopLevelCallback: function createTopLevelCallback(topLevelType) {
    return function (nativeEvent) {
		if (nativeEvent.type === 'click') {
			console.log('document native click callback');
		}

      if(! _topLevelListenersEnabled) {return;
      }
      // TODO: Remove when synthetic events are ready, this is for IE<9.
      if(nativeEvent.srcElement && nativeEvent.srcElement ! == nativeEvent.target) { nativeEvent.target = nativeEvent.srcElement; } var topLevelTarget = ReactMount.getFirstReactDOM(getEventTarget(nativeEvent)) || window; var topLevelTargetID = ReactMount.getID(topLevelTarget) ||' ';
      ReactEventEmitter.handleTopLevel(topLevelType, topLevelTarget, topLevelTargetID, nativeEvent);
    };
  }
Copy the code

The final printed result is:

document native click callback
btn react clickCapture event
btn react click event
Copy the code

Conclusion 1 is verified to be correct.

Next, let’s verify conclusion 2 again:

Before we verify this, we need to understand the phenomenon that “a user interaction may trigger multiple events”. For example, an interactive action like “click a button” might trigger “mousedown”, “mouseup”, or “click” for a button element. The React composite event system also has multiple “focus” events.

handleMousedown=()=> {console.log('react mousedown event')}
handleMouseup=()=> {console.log('react mouseup event')}
handleClick=()=> {console.log('react click event')}}
render() {
    return (
        <button 
            id="btn"OnMouseDown ={this.handlemousedown} onMouseUp={this.handlemouseup} onClick={this.handleclick} > </button>)}componentDidMount(){
    const btn = doucument.getElementById('btn');
    btn.addEventListener('mousedown',()=> { console.log('native mousedown event')});
    btn.addEventListener('mouseup',()=> { console.log('native mouseup event')});
    btn.addEventListener('click',()=> { console.log('native click event')});
}
Copy the code

The print result is as follows:

native mousedown event
react mousedown event
native mouseup event
react mouseup event
native click event
react click event
Copy the code

You can see that the react event Listener is called in the same order as the original event Listener. Conclusion 2 is correct.

Because the event delegate mode relies on the browser’s native event bubbling mechanism, the react event Listener will not execute if we prevent the event from propagating in the event’s bubbling path. To verify this, use the following code:

handleClick=()=> {console.log('react click event')}}
render() {
    return (
        <button 
            id="btn"OnMouseDown ={this.handlemousedown} onMouseUp={this.handlemouseup} onClick={this.handleclick} > </button>)}componentDidMount(){
    const btn = doucument.getElementById('btn');
    btn.addEventListener('click',(event)=> { 
        event.stopPropragation();
    });
}
Copy the code

After the above code is executed, you will notice that the React event listener is not executed when you click button. This is because the topLevelCallback registered on the Document object was not executed. If we annotate the event.stopPropragation() statement, the console will reprint the React Click event. This proves that the event delegate mode is a bit of a trap: if you accidentally write code that combines native event listeners with React event listeners during development, all react Event listeners in your Button event path will not be executed.

Now that the first phase of the React synthesis system has been explained, let’s move on to the second phase.

Storage stage

The event Listener registered by the user (that is, the developer) is generally called “Application Event Listener”. In the following, we refer to the event Listener for short. The storage phase is the process of collecting react Elements and storing them in the event listener registry.

First, take a look at how we register event listeners in React. If written as JSX, it looks like this:

handleClick=()=> {console.log('react click event')}}
render() {
    return (
        <button 
            id="btn"OnClick ={this.handleclick} > </button>)}Copy the code

The react event listener can be seen more clearly if we write it as js:

handleClick=()=> {console.log('react click event')}}
render() {
    return React.DOM.button({
        id: 'btn',
        onClick: this.handleClick
    }, 'Click and I'll try it.');
}
Copy the code

JSX is written much like DOM1 event listening, and if we don’t think about it, we can easily be confused by feelings. I thought I was writing some native event listening code. It’s not. After all, the react event listener notation is essentially a key-value pair in an object. The key is “onClick” and the value is the function reference to the Event Listener. Using functions as values is a hallmark of javascript programming. Therefore, I can assume, without reading the source code, that the Event Listener function will be collected and stored temporarily by some third party.

Since “onClick” is a prop for the React Element, and the event-listening prop is only useful if it is written to the reactDOMComponent, we should look at the code for the reactDOMComponent. In the _createOpenTagMarkup method of reactDomComponent.js, we see a line that looks like this:

_createOpenTagMarkup: function() {/ /...if(registrationNames[propKey]) { putListener(this._rootNodeID, propKey, propValue); } / /...Copy the code

What is registrationNames? We traced it back to a collection of event names prefixed with “on” after changing the event names supported by the current browser to a small hump. Print it out and it looks like this:

Okay, so now that we know when to collect react, we’re going to figure out how to collect react. After some code navigation, we finally arrive at our destination: the putListener method of CallbackRegistry.

/*
* @param {string} id ID of the DOM element.
* @param {string} registrationName Name of listener (e.g. `onClick`).
* @param {?function} listener The callback to store.
*/
putListener: function(id, registrationName, listener) {
    var bankForRegistrationName =
    listenerBank[registrationName] || (listenerBank[registrationName] = {});
    bankForRegistrationName[id] = listener;
}
Copy the code

Wait, what about the listenerBank variable? Moving up, we see:

var listenerBank = {};
Copy the code

Yes, this is a global variable in the CallbackRegistry module. That’s where React stores event Listeners for us. The source code comments also make it clear:

/**
 * Stores "listeners" by `registrationName`/`id`. There should be at most one
 * "listener" per `registrationName`/`id` in the `listenerBank`.
 *
 * Access listeners via `listenerBank[registrationName][id]`.
 */
Copy the code

Speaking so frankly, I won’t be verbose here. The listenerBank data structure after storing the Event Listener looks like this:

listenerBank = {
   onClick: {
       '[0]. [1]': listener // Listener is the event callback that we mount in JSX}Copy the code

To get a better impression of the listenerBank data structure, we printed out the listenerBank data structure in practice:

A string like ‘[0].[1]’ is a reactid value (reactid will be removed in later versions?) , corresponding to a real DOM element rendered by React on the page.

After the above detailed analysis, we can summarize the collection process of event Listener as follows:

  1. Users register event listeners on react Elements to establish a DOM element -> event name -> event listener relationship.
  2. React collects and organizes these relationships in listenerBank module global variables when the component is initially mounted. How do you organize it? That is, the first layer of the listenerBank object is categorized by registered event type, and then categorized by Reactid within the same event type.

This is the end of phase 2 of the React synthetic event system. We just need to remember the listenerBank object’s data structure so that we can understand it better when phase 3 involves fetching event Listeners.

In fact, compared to the call stage of the Event Listener (the third stage), the first and second stages mentioned above can be regarded as preparatory work. Because, so far, our Event Listener has been sleeping in the arms of listenerBank.

Call stage

Here comes the big one. From the function signature void func(event) of the Event Listener, we can know that we have the following two exploration points in the fourth stage:

  1. Synthesis of the event object;
  2. Call Event ListEnter.

In fact, handleTopLevel, the call phase entry function, does both of these things:

/**
* Streams a fired top-level event to `EventPluginHub` whereplugins have the * opportunity to create `ReactEvent`s to be dispatched. * * @param {string} topLevelType Record from 'EventConstants'. * @param {object} topLevelTarget The Listening Component root node.  * @param {string} topLevelTargetID ID of `topLevelTarget`. * @param {object} nativeEvent Native environment event. */ handleTopLevel:function( topLevelType, topLevelTarget, topLevelTargetID, nativeEvent) { // 1. Synthesis of the event object var events = EventPluginHub. ExtractEvents (topLevelType topLevelTarget, topLevelTargetID, nativeEvent);  / / 2. Call the event listenter ReactUpdates. BatchedUpdates (runEventQueueInBatch, events); }Copy the code

There are two steps for “event Object synthesis” :

  1. Instantiate and combine into event objects.
  2. The event Listener to be called is stored on this object.

All of the Core eventPlugin implements these two functional requirements. Let’s take a look at two or three plugins.

  1. SimpleEventPlugin
 extractEvents: function(
      topLevelType,
      topLevelTarget,
      topLevelTargetID,
      nativeEvent) {
    var dispatchConfig = topLevelEventsToDispatchConfig[topLevelType];
    if(! dispatchConfig) {return null;
    }
    var EventConstructor;
    switch(topLevelType) {
      case topLevelTypes.topInput:
      case topLevelTypes.topSubmit:
        // HTML Events
        // @see http://www.w3.org/TR/html5/index.html#events-0
        EventConstructor = SyntheticEvent;
        break;
      case topLevelTypes.topKeyDown:
      case topLevelTypes.topKeyPress:
      case topLevelTypes.topKeyUp:
        EventConstructor = SyntheticKeyboardEvent;
        break;
      case topLevelTypes.topBlur:
      case topLevelTypes.topFocus:
        EventConstructor = SyntheticFocusEvent;
        break;
      case topLevelTypes.topClick:
        // Firefox creates a click event on right mouse clicks. This removes the
        // unwanted click events.
        if (nativeEvent.button === 2) {
          return null;
        }
        /* falls through */
      case topLevelTypes.topContextMenu:
      case topLevelTypes.topDoubleClick:
      case topLevelTypes.topDrag:
      case topLevelTypes.topDragEnd:
      case topLevelTypes.topDragEnter:
      case topLevelTypes.topDragExit:
      case topLevelTypes.topDragLeave:
      case topLevelTypes.topDragOver:
      case topLevelTypes.topDragStart:
      case topLevelTypes.topDrop:
      case topLevelTypes.topMouseDown:
      case topLevelTypes.topMouseMove:
      case topLevelTypes.topMouseUp:
        EventConstructor = SyntheticMouseEvent;
        break;
      case topLevelTypes.topTouchCancel:
      case topLevelTypes.topTouchEnd:
      case topLevelTypes.topTouchMove:
      case topLevelTypes.topTouchStart:
        EventConstructor = SyntheticTouchEvent;
        break;
      case topLevelTypes.topScroll:
        EventConstructor = SyntheticUIEvent;
        break;
      case topLevelTypes.topWheel:
        EventConstructor = SyntheticWheelEvent;
        break;
      case topLevelTypes.topCopy:
      case topLevelTypes.topCut:
      case topLevelTypes.topPaste:
        EventConstructor = SyntheticClipboardEvent;
        break;
    }
    ("production"! == process.env.NODE_ENV ? invariant( EventConstructor,'SimpleEventPlugin: Unhandled event type, `%s`.',
      topLevelType
    ) : invariant(EventConstructor));
    var event = EventConstructor.getPooled(
      dispatchConfig,
      topLevelTargetID,
      nativeEvent
    );
    EventPropagators.accumulateTwoPhaseDispatches(event);
    return event;
  }
Copy the code

Let’s focus on the last three lines:

var event = EventConstructor.getPooled(
      dispatchConfig,
      topLevelTargetID,
      nativeEvent
    );
    EventPropagators.accumulateTwoPhaseDispatches(event);
    return event;
Copy the code

Above, in the extractEvents method, the omitted code basically does one thing: calculates the constructor of the corresponding synthesized event object based on the value of topLevelTypes. Next, as we have seen, EventConstructor getPooled () call returns an instance – synthetic event object. Instantiate a normal function call instead of using the new operator? This is because the technique of object pooling is used. As for pooling, as mentioned above, the technical details are not discussed. A function call the penultimate line: EventPropagators. AccumulateTwoPhaseDispatches (event); Is to do what step two does. This function call results in a short stack of function calls like this:

getListener()  

listenerAtPhase()

accumulateDirectionalDispatches()

traverseParentPath()

traverseTwoPhase()

accumulateTwoPhaseDispatchesSingle()

forEachAccumulated()

accumulateTwoPhaseDispatches()
Copy the code

This call stack is a key part of “composing event Objects”, and we’ll come back to it once we’ve sampled the rest of the eventPlugin. Now, let’s continue sampling.

  1. SelectEventPlugin(source code extracted from selecteventplugin.js)
extractEvents: function(
  topLevelType,
  topLevelTarget,
  topLevelTargetID,
  nativeEvent) {

switch (topLevelType) {
  // Track the input node that has focus.
  case topLevelTypes.topFocus:
    if (isTextInputElement(topLevelTarget) ||
        topLevelTarget.contentEditable === 'true') {
      activeElement = topLevelTarget;
      activeElementID = topLevelTargetID;
      lastSelection = null;
    }
    break;
  case topLevelTypes.topBlur:
    activeElement = null;
    activeElementID = null;
    lastSelection = null;
    break;

  // Do not fire the event while the user is dragging. This matches the
  // semantics of the native select event.
  case topLevelTypes.topMouseDown:
    mouseDown = true;
    break;
  case topLevelTypes.topContextMenu:
  case topLevelTypes.topMouseUp:
    mouseDown = false;
    return constructSelectEvent(nativeEvent);

  // Chrome and IE fire non-standard event when selection is changed (and
  // sometimes when it has not).
  case topLevelTypes.topSelectionChange:
    return constructSelectEvent(nativeEvent);

  // Firefox does not support selectionchange, so check selection status
  // after each key entry.
  case topLevelTypes.topKeyDown:
    if(! useSelectionChange) { activeNativeEvent = nativeEvent;setTimeout(dispatchDeferredSelectEvent, 0);
    }
    break; }}Copy the code

The implementation of constructSelectEvent looks like this:

function constructSelectEvent(nativeEvent) {
  // Ensure we have the right element, and that the user is not dragging a
  // selection (this matches native `select` event behavior).
  if(mouseDown || activeElement ! = getActiveElement()) {return;
  }

  // Only fire when selection has actually changed.
  var currentSelection = getSelection(activeElement);
  if(! lastSelection || ! shallowEqual(lastSelection, currentSelection)) { lastSelection = currentSelection; var syntheticEvent = SyntheticEvent.getPooled( eventTypes.select, activeElementID, nativeEvent ); syntheticEvent.type ='select';
    syntheticEvent.target = activeElement;

    EventPropagators.accumulateTwoPhaseDispatches(syntheticEvent);

    returnsyntheticEvent; }}Copy the code

Looking closer, we see the same code “paradigm” again:

var syntheticEvent = SyntheticEvent.getPooled(
      eventTypes.select,
      activeElementID,
      nativeEvent
    );

    syntheticEvent.type = 'select';
    syntheticEvent.target = activeElement;

    EventPropagators.accumulateTwoPhaseDispatches(syntheticEvent);

    return syntheticEvent;
Copy the code

Uh huh, is SyntheticEvent getPooled () and EventPropagators accumulateTwoPhaseDispatches ();

  1. ChangeEventPlugin(source code extracted from selecteventplugin.js)
extractEvents: function(
      topLevelType,
      topLevelTarget,
      topLevelTargetID,
      nativeEvent) {

    var getTargetIDFunc, handleEventFunc;
    if (shouldUseChangeEvent(topLevelTarget)) {
      if (doesChangeEventBubble) {
        getTargetIDFunc = getTargetIDForChangeEvent;
      } else{ handleEventFunc = handleEventsForChangeEventIE8; }}else if (isTextInputElement(topLevelTarget)) {
      if (isInputEventSupported) {
        getTargetIDFunc = getTargetIDForInputEvent;
      } else{ getTargetIDFunc = getTargetIDForInputEventIE; handleEventFunc = handleEventsForInputEventIE; }}else if (shouldUseClickEvent(topLevelTarget)) {
      getTargetIDFunc = getTargetIDForClickEvent;
    }

    if (getTargetIDFunc) {
      var targetID = getTargetIDFunc(
        topLevelType,
        topLevelTarget,
        topLevelTargetID
      );
      if (targetID) {
        var event = SyntheticEvent.getPooled(
          eventTypes.change,
          targetID,
          nativeEvent
        );
        EventPropagators.accumulateTwoPhaseDispatches(event);
        returnevent; }}if(handleEventFunc) { handleEventFunc( topLevelType, topLevelTarget, topLevelTargetID ); }}Copy the code

Yes, again:

if (targetID) {
        var event = SyntheticEvent.getPooled(
          eventTypes.change,
          targetID,
          nativeEvent
        );
        EventPropagators.accumulateTwoPhaseDispatches(event);
        return event;
      }
Copy the code

Sampling completed. As we concluded above, the two core functional requirements that all EventPlugins implement are:

  1. Instantiate and combine into event objects.
  2. The event Listener to be called is stored on this object.

This process involves many implementation details, such as compatibility details for browser differences, pooling techniques, and so on. At the same time, different event types do different browser compatibilities, and different event types implement different constructors. There are too many details, not too closely related to our main line, so I won’t go into it. Those who are interested can study separately. The important thing here is that the Event Listener to be called in step 2 is stored on this object. To put it bluntly, we’re going to explore the function call boardwalk mentioned above in analyzing the SimpleEventPlugin:

ListenerAtPhase getListener () / / stack accumulateDirectionalDispatches () () traverseParentPath traverseTwoPhase () () accumulateTwoPhaseDispatchesSingle()forAccumulateTwoPhaseDispatches EachAccumulated () () / / bottom of stackCopy the code

From the getListener() method name at the top of the call stack, we’re on the right track. Because the action of collecting the Event Listener should happen regardless of the implementation. So, next, we take “call phase, how does the event Listener collection process work?” The question continues to be explored.

First of all, we look at accumulateTwoPhaseDispatches function signature: void func (events). From the function signature, we can see that the parameter is called events. According to the debugging results, the data type of events can be either a single Event object or an array composed of multiple event Objects. In most cases, we see a single Event Object. And when is it an array? I haven’t worked it out yet. I’ll do it some other time. From accumulateTwoPhaseDispatches () the method name, we learn that the process is in each incoming event object to accumulate (the accumulate) event listenter process. Because the events argument we are talking about here is a case of a single object, the forEachAccumulated () method is useless. Why do you say that? See its code is implemented to know:

/**
 * @param {array} an "accumulation" of items which is either an Array or
 * a single item. Useful when paired with the `accumulate` module. This is a
 * simple utility that allows us to reason about a collection of items, but
 * handling the case when there is exactly one item (and we do not need to
 * allocate an array).
 */

var forEachAccumulated = function forEachAccumulated(arr, cb, scope) {
  if (Array.isArray(arr)) {
    arr.forEach(cb, scope);
  } else if(arr) { cb.call(scope, arr); }};Copy the code

In our case, the code ends up executing to the else if branch. Will say, the final result will come accumulateTwoPhaseDispatchesSingle (event) this method call:

/**
 * Collect dispatches (must be entirely collected before dispatching - see unit
 * tests). Lazily allocate the array to conserve memory.  We must loop through
 * each event and perform the traversal for each one. We can not perform a
 * single traversal forThe entire collection of events because each event may * have a different target. * The single in the method name refers to each event object */function accumulateTwoPhaseDispatchesSingle(event) {
  if(event && event.dispatchConfig.phasedRegistrationNames) { injection.InstanceHandle.traverseTwoPhase(event.dispatchMarker, accumulateDirectionalDispatches, event); }}Copy the code

A accumulated event Listener (also called a “accumulated event Dispatches”) is a modified event object instantiated with two fields: _dispatchIDs and _dispatchListeners are used to save the reactId and event Listeners registered on the DOM node where the Event object is to be distributed, respectively. The cumulative process starts with the event target that triggered the event and walks through its capture and bubbling phases to collect the associated reactId and Event Listener. That’s what the “two phases” in the method name means. As for “single” in method name, it refers to “each single event object” in events. Note that the Event Object has a dispatchMarker field, which is the reactId on the Event Target.

Next, the incoming traverseTwoPhase(targetID, CB, ARG) method is responsible for the actual traversal:

/**
* Simulates the traversal of a two-phase, capture/bubble event dispatch.
*
* NOTE: This traversal happens on IDs without touching the DOM.
*
* @param {string} targetID ID of the target node.
* @param {function} cb Callback to invoke.
* @param {*} arg Argument to invoke the callback with.
* @internal
*/
traverseTwoPhase: function traverseTwoPhase(targetID, cb, arg) {
// console.log('targetID:', targetID);
if (targetID) {
  traverseParentPath(' ', targetID, cb, arg, true.false);
  traverseParentPath(targetID, ' ', cb, arg, false.true); }},Copy the code

Void function traverseParentPath(start, stop, cb, arg, skipFirst, skipLast); TraverseParentPath (“, targetID, cb, arg, true, false); Iterating through the capture phase of event target event propagation; TraverseParentPath (targetID, “, cb, arg, false, true); } means traversing the bubbling phase of event Target event propagation (the empty string passing the parameter represents the parent of the farthest ancestor element in the Event Target hierarchy). Note that in a complete event propagation, the capture phase is followed by the bubble phase, which is determined by the sequence of these two lines of code. Don’t believe it? So let’s test the following. We registered both the bubbling event and the capture event on the same element, and logged the event listener as follows:

As you can see, the order has changed. This proves that my conclusion is correct. Ok, now it’s the turn of the traverseParentPath method to do the actual for loop. Let’s have a look at its source code:

/** * Traverses the parent path between two IDs (either up or down). The IDs must * not be the same, and there must exist a parent path between them. * * @param {? string} start ID atwhichto start traversal. * @param {? string} stop ID atwhich to end traversal.
 * @param {function} cb Callback to invoke each ID with. * @param {? boolean} skipFirst Whether or not to skip the first node. * @param {? boolean} skipLast Whether or not to skip the last node. * @private */function traverseParentPath(start, stop, cb, arg, skipFirst, skipLast) {
  start = start || ' ';
  stop = stop || ' ';
  "production"! == process.env.NODE_ENV ? invariant(start ! == stop,'traverseParentPath(...) : Cannot traverse from and to the same ID, `%s`.', start) : invariant(start ! == stop); var traverseUp = isAncestorIDOf(stop, start);"production"! == process.env.NODE_ENV ? invariant(traverseUp || isAncestorIDOf(start, stop),'traverseParentPath(%s, %s, ...) : Cannot traverse from two IDs that do ' + 'not have a parent path.', start, stop) : invariant(traverseUp || isAncestorIDOf(start, stop));
  // Traverse from `start` to `stop` one depth at a time.
  var depth = 0;
  var traverse = traverseUp ? getParentID : getNextDescendantID;
  for (var id = start;; /* until break */id = traverse(id, stop)) {
    if((! skipFirst || id ! == start) && (! skipLast || id ! == stop)) { cb(id, traverseUp, arg); }if (id === stop) {
      // Only break //after// visiting `stop`.
      break;
    }
    "production"! == process.env.NODE_ENV ? invariant(depth++ < MAX_TREE_DEPTH,'traverseParentPath(%s, %s, ...) : Detected an infinite loop while ' + 'traversing the React DOM ID tree. This may be due to malformed IDs: %s', start, stop) : invariant(depth++ < MAX_TREE_DEPTH); }}Copy the code

See the for loop? Listeners are added to the event._dispatchListeners array one by one in the for loop. The first if in the for loop can actually be converted to:

if(! ((skipFirst && id === start) || (skipLast && id === stop))) { cb(id, traverseUp, arg); }Copy the code

That is, event Listeners on all nodes except the parent node of the farthest ancestor element on the propagation path are collected. Cb (ID, traverseUp, ARG); Means accumulateDirectionalDispatches (domID, upwards, the event). It is this method that is responsible for traversal based on domID and phase (traversing up is bubbling; The event listener is found on the process, and the listeners are added to the event._dispatchlisteners. Let’s look at the source code here:

/**
 * Tags a `SyntheticEvent` with dispatched listeners. Creating this function
 * here, allows us to not have to bind or create functions for each event.
 * Mutating the event  members allows us to not have to create a wrapping
 * "dispatch" object that pairs the event with the listener.
 */
function accumulateDirectionalDispatches(domID, upwards, event) {
  if ("production"! == process.env.NODE_ENV) {if(! domID) { throw new Error('Dispatching id must not be null');
    }
    injection.validate();
  }
  var phase = upwards ? PropagationPhases.bubbled : PropagationPhases.captured;
  var listener = listenerAtPhase(domID, event, phase);
  if(listener) { event._dispatchListeners = accumulate(event._dispatchListeners, listener); event._dispatchIDs = accumulate(event._dispatchIDs, domID); }}Copy the code
  1. The event Listener is searched for by the following statement:
var listener = listenerAtPhase(domID, event, phase);
Copy the code
  1. The event listener is queued to the event._dispatchlisteners
if (listener) {
    event._dispatchListeners = accumulate(event._dispatchListeners, listener);
    event._dispatchIDs = accumulate(event._dispatchIDs, domID);
  }
Copy the code

The listenerAtPhase(domID, Event, Phase) finally calls the getListener method, based on the domID (essentially reactId) and the phase event name (e.g., bubble phase: onClick; Capture stage: onClickCapture) look for the Event Listener in the listenerBank event registry that we mentioned in the first stage. If the Event Listener is found again, it is enqueued. The enrolling operation is done by the accumulate () method, which is essentially a concat of an array.

At this point, we’ve basically combed through the flow involved in the “instantiate into event objects” step. TraverseTwoPhase is the most important call on the stack of functions that this process corresponds to. It is in the call stack above this function that React ensures that event listeners are queued in both directions. What are the two orders? The first is to register the event Listener in the capture phase before the event Listener in the bubble phase. The second is that event listeners registered at each event propagation stage should be enqueued in the correct order.

The guarantee of the first order has been mentioned above. This is done in the order of the following two statements:

  traverseParentPath(' ', targetID, cb, arg, true.false);
  traverseParentPath(targetID, ' ', cb, arg, false.true);
Copy the code

The second sequential guarantee is guaranteed in the for loop by traversing up or down the hierarchical chain of a given event target and joining the queue one by one. Specific is through the following code to achieve:

var traverse = traverseUp ? getParentID : getNextDescendantID;

for (var id = start;; /* until break */id = traverse(id, stop)) {
    if((! skipFirst || id ! == start) && (! skipLast || id ! == stop)) { cb(id, traverseUp, arg); }if (id === stop) {
      // Only break //after// visiting `stop`.
      break; })Copy the code

So far, the event Listeners we need to call are stored in the event._dispatchListeners array without problem. Everything waits for the React call. So, let’s talk about the second step: “Call event ListEnter.”

The entry to the call process is this code:

ReactUpdates.batchedUpdates(runEventQueueInBatch, events);
Copy the code

The corresponding function call stack is:

The calling process takes place in a transaction. The Transaction pattern is similar to a Wrapper in that its main function is to invoke a core method. Since this article dives into the React synthetic event system, I’m not going to explain the patterns and principles of transaction. We just need to know that the core method associated with the event Listener call process is the “runEventQueueInBatch” method. Without transaction, the event Listener call process is simple and can be summarized as: two variables, two loops.

What are the two variables? Answer:

  • eventQueue
  • event._dispatchListeners

Both eventQueue and Event._dispatchListeners are queues (implemented as arrays in javascript). As mentioned above, when an eventQueue is an array, the array is made up of Event Objects (SyntheticEvent instances). The _dispatchListeners of the Event Object are composed of our Event Listeners. In the call stack, we can find these two methods responsible for doing the loop:

  • forEachAccumulated()
  • forEachEventDispatch()
EventQueue | | -- event1 | -- event2 | --...... | - eventn. _dispatchListeners | | - listener1 | - listener1 | -... |--- listenernCopy the code

The relationship between the two is illustrated above. Therefore, it is not surprising that to call the Event Listener, you need to loop twice (similar to a double loop for a two-bit array). As you can see from the method name, the methods in the call stack responsible for these two loops are:

  • forEachAccumulated(arr, cb, scope)
  • forEachEventDispatch(event, cb)

In general, eventQueue is only an Event object, so forEachAccumulated(ARR, CB, Scope) is not much to talk about. Because the forEachEventDispatch(event, CB) loop has a very important implementation, that is “prevent event propagation” event mechanism implementation. Let’s focus on the implementation code for this method:

/**
 * Invokes `cb(event, listener, id)`. Avoids using call if no scope is
 * provided. The `(listener,id)` pair effectively forms the "dispatch" but are
 * kept separate to conserve memory.
 */
function forEachEventDispatch(event, cb) {
  var dispatchListeners = event._dispatchListeners;
  var dispatchIDs = event._dispatchIDs;
  if ("production"! == process.env.NODE_ENV) { validateEventDispatches(event); }if (Array.isArray(dispatchListeners)) {
    for (var i = 0; i < dispatchListeners.length; i++) {
      if (event.isPropagationStopped()) {
        break; } cb(event, dispatchListeners[i], dispatchIDs[i]); }}else if(dispatchListeners) { cb(event, dispatchListeners, dispatchIDs); }}Copy the code

A big for loop comes into view, as you can see. [I], dispatchListeners[I]); Essentially executeDispatches (events, dispatchListeners[I]) that actually call the event listener (using the call operator), DispatchIDs [I]), and a bland “break” keyword is the soul of an event mechanism that “prevents event propagation”. When the isPropagationStopped method of the Event Object returns true, we break out of the loop, and all event listeners following the queue are not executed, thus implementing the event mechanism of “preventing event propagation”. When does the isPropagationStopped method return true? Let’s do a global search to see its implementation code (at SyntheticEvent.js) :

  stopPropagation: function() {
    var event = this.nativeEvent;
    event.stopPropagation ? event.stopPropagation() : event.cancelBubble = true;
    this.isPropagationStopped = emptyFunction.thatReturnsTrue;
  },
Copy the code

React appends a field to the event object when the user manually calls the stopPropagation method in the previous event Listener. The value of the field is a function reference, which returns true. Therefore, when the for loop execution to the next cycle, isPropagationStopped pointing emptyFunction. ThatReturnsTrue, if the condition is true, then jump out the systemic circulation.

Well, we’ve covered how the react synthetic event system implements the event mechanism that prevents event propagation. So let’s move on.

As mentioned above, the real method responsible for calling event listeners (using the call operator) is executeDispatch(event, dispatchListeners[I], dispatchIDs[I]). This executeDispatch method is actually a function reference. What it means can be seen in the following code

/** * Dispatches an event and releases it back into the pool, unless persistent. * * @param {? object} event Synthetic event to be dispatched. * @private */ var executeDispatchesAndRelease =function executeDispatchesAndRelease(event) {
  if (event) {
    var executeDispatch = EventPluginUtils.executeDispatch;
    // Plugins can provide custom behavior when dispatching events.
    var PluginModule = EventPluginRegistry.getPluginModuleForEvent(event);
    if (PluginModule && PluginModule.executeDispatch) {
      executeDispatch = PluginModule.executeDispatch;
    }
    EventPluginUtils.executeDispatchesInOrder(event, executeDispatch);

    if(! event.isPersistent()) { event.constructor.release(event); }}};Copy the code

In combination with the above comment “Plugins can provide custom behavior when Dispatching Events.” and in eventPluginHub.js

/**
 * This is a unified interface for event plugins to be installed and configured.
 *
 * Event plugins can implement the following properties:
 *
 *   `extractEvents` {function(string, DOMEventTarget, string, object): *}
 *     Required. When a top-level event is fired, this method is expected to
 *     extract synthetic events that will in turn be queued and dispatched.
 *
 *   `eventTypes` {object}
 *     Optional, plugins that fire events must publish a mapping of registration
 *     names that are used to register listeners. Values of this mapping must
 *     be objects that contain `registrationName` or `phasedRegistrationNames`.
 *
 *   `executeDispatch` {function(object, function, string)}
 *     Optional, allows plugins to override how an event gets dispatched. By
 *     default, the listener is simply invoked.
 *
 * Each plugin that is injected into `EventsPluginHub` is immediately operable.
 *
 * @public
 */
var EventPluginHub = {
    // ......
}
Copy the code

As you can see, the final executeDispatch reference is calculated as follows: if the so-and-so eventPlugin implements this method, it will be used first. Otherwise, the default method is used. What is the default executeDispatch? In the original file eventpluginutils.js, we found it:

/**
 * Default implementation of PluginModule.executeDispatch().
 * @param {SyntheticEvent} SyntheticEvent to handle
 * @param {function} Application-level callback
 * @param {string} domID DOM id to pass to the callback.
 */
function executeDispatch(event, listener, domID) {
  listener(event, domID);
}
Copy the code

As you can see, the default executeDispatch implementation is the simplest, which means using the function call operator to manipulate our Event Listener.

Looking at all the EventPlugins, only SimpleEventPlugin seems to have implemented its own executeDispatch method:

/**
* Same as the default implementation, except cancels the event when return
* value is false.
*
* @param {object} Event to be dispatched.
* @param {function} Application-level callback.
* @param {string} domID DOM ID to pass to the callback.
*/
executeDispatch: function(event, listener, domID) {
    var returnValue = listener(event, domID);
    if (returnValue === false) { event.stopPropagation(); event.preventDefault(); }},Copy the code

Since SimpleEventPlugin handles most of the event types, the above reference would normally point to SimpleEventPlugin’s executeDispatch method.

Let’s look at the if conditional statement:

if (returnValue === false) {
      event.stopPropagation();
      event.preventDefault();
}
Copy the code

In the react development process, the event listener returns false to prevent event propagation and cancel the default behavior using this code. React calls the stopPropagation method on the Event Object. If the event Listener returns false, react calls the stopPropagation method on the Event object. So, here’s what we can conclude: In react, if you want to prevent events from spreading, you have two ways to do it:

  • In the Event Listener, call event.stopPropagation() manually;
  • In the Event Listener, react calls event.stopPropagation() by returning false. (In v0.12.0, this was deprecated. You can search globally in this blog for “DEPRECATED Returning false from event Handlers to preventDefault”);

Now we see the call to the Event Listener explicitly:

listener(event, domID)
Copy the code

From the above code, we can see that in reactV0.8.0, our event Listener was actually passed two arguments, but the second parameter, reactId, was not used at the time.

At this point, we have combed through the end of the process of calling the Event Listener, which means that the overall analysis of stage 3 is complete. There are several research focuses in the third phase, which are reviewed below:

  1. How are event Listeners collected?
  2. How is the order in which event Listeners are called guaranteed?
  3. How is the event propagation prevention mechanism implemented?
  4. Return false in event Listener. What does react do for us?

Finishing touches

The finalizing phase mainly frees the memory occupied by eventQueue and event Object (the current Event Loop dispatch). In javascript, freeing memory is nothing more than assigning a variable to NULL.

First, let’s look at the memory release of eventQueue (in eventpluginhub.js) :

 processEventQueue: function() {
    // Set `eventQueue` to null before processing it so that we can tell if more
    // events get enqueued while processing.
    var processingEventQueue = eventQueue;
    eventQueue = null; 
    forEachAccumulated(processingEventQueue, executeDispatchesAndRelease);
    ("production"! == process.env.NODE_ENV ? invariant( ! eventQueue,'processEventQueue(): Additional events were enqueued while processing ' +
      'an event queue. Support for this has not yet been implemented.') : invariant(! eventQueue)); }Copy the code

Next, let’s look at the memory freeing of the Event Object.

The _dispatchListeners and _dispatchIDs queues are cleaned up after all the event listeners have been executed:

/** * Standard/ Simple Iteration Through an event s collected dispatches. * * /function executeDispatchesInOrder(event, executeDispatch) {
  forEachEventDispatch(event, executeDispatch);

  event._dispatchListeners = null;
  event._dispatchIDs = null;
}
Copy the code

The second step is to release memory by pooling technology:

var executeDispatchesAndRelease = function executeDispatchesAndRelease(event) {
  if (event) {
    var executeDispatch = EventPluginUtils.executeDispatch;
    // Plugins can provide custom behavior when dispatching events.
    var PluginModule = EventPluginRegistry.getPluginModuleForEvent(event);
    if (PluginModule && PluginModule.executeDispatch) {
      executeDispatch = PluginModule.executeDispatch;
    }
    EventPluginUtils.executeDispatchesInOrder(event, executeDispatch);

    if(! event.isPersistent()) { event.constructor.release(event); }}};Copy the code

If the user does not manually persist the event object (event.isPersistent=function(){return true}), the event object will be released. How to release it? For example, if the current Event Object is an instance of a SyntheticMouseEvent, event.constructor means the SyntheticMouseEvent class:

function SyntheticMouseEvent(dispatchConfig, dispatchMarker, nativeEvent) {
  SyntheticUIEvent.call(this, dispatchConfig, dispatchMarker, nativeEvent);
}
Copy the code

As can be seen from the above, SyntheticMouseEvent essentially inherits SyntheticUIEvent, which in turn inherits SyntheticEvent. We expected to find the implementation code for the Release method in the SyntheticEvent code, but it was found in pooledClass.js. Why is that? Because the react release method is a static method added dynamically after a SyntheticEvent is added to the pooling pool:

PooledClass.addPoolingTo(SyntheticEvent, PooledClass.threeArgumentPooler);
Copy the code

The code for the addPoolingTo method looks like this:

/**
 * Augments `CopyConstructor` to be a poolable class, augmenting only the class
 * itself (statically) not adding any prototypical fields. Any CopyConstructor
 * you give this may have a `poolSize` property, and will look for a
 * prototypical `destructor` on instances (optional).
 *
 * @param {Function} CopyConstructor Constructor that can be used to reset.
 * @param {Function} pooler Customizable pooler.
 */
var addPoolingTo = function(CopyConstructor, pooler) {
  var NewKlass = CopyConstructor;
  NewKlass.instancePool = [];
  NewKlass.getPooled = pooler || DEFAULT_POOLER;
  if(! NewKlass.poolSize) { NewKlass.poolSize = DEFAULT_POOL_SIZE; } NewKlass.release = standardReleaser;return NewKlass;
};
Copy the code

See? NewKlass.release = standardReleaser; The “NewKlass” statement refers to a SyntheticEvent. So, in the end, the event. The constructor. The release of the release point to an standardReleaser. So let’s see what standardReleaser looks like:

var standardReleaser = function(instance) {
  var Klass = this;
  if (instance.destructor) {
    instance.destructor();
  }
  if(Klass.instancePool.length < Klass.poolSize) { Klass.instancePool.push(instance); }};Copy the code

The instance argument is the SyntheticMouseEvent instance in the argument phase. So we look for the destructor method along the prototype chain of the SyntheticMouseEvent instance and find it (in SyntheticEvent.js) :

  /**
   * `PooledClass` looks for `destructor` on each instance it releases.
   */
  destructor: function() {
    var Interface = this.constructor.Interface;
    for (var propName in Interface) {
      this[propName] = null;
    }
    this.dispatchConfig = null;
    this.dispatchMarker = null;
    this.nativeEvent = null;
  }
Copy the code

We can see that the event object’s memory is freed mainly from the memory referenced by its fields, but not from the memory occupied by the event object itself. The event object is eventually managed by the pooling technique, that is, it is eventually reclaimed into the instance pool, as shown in the following code snippet in the standardReleaser method:

if (Klass.instancePool.length < Klass.poolSize) {
    Klass.instancePool.push(instance);
  }
Copy the code

At this point, the end of the analysis is done. More on that. How does react retrieve an instance from the instance pool when the next event loop starts? In fact, this connects back to the first step of our third stage: synthesizing event Objects. For each event loop, the extractEvent method is re-executed to synthesize an Event object, and the extractEvent method retrieves a line of instance code from the instance pool, for example:

var event = SyntheticEvent.getPooled(
      eventTypes.change,
      targetID,
      nativeEvent
);
Copy the code

The getPooled method is determined by adding the class (e.g. SyntheticEvent) to the pooling pool during code initialization. Or what we mentioned above addPoolingTo method call in the incoming PooledClass. ThreeArgumentPooler method. Then we will look at PooledClass. ThreeArgumentPooler this method the implementation of the code:

var threeArgumentPooler = function(a1, a2, a3) {
  var Klass = this;
  if (Klass.instancePool.length) {
    var instance = Klass.instancePool.pop();
    Klass.call(instance, a1, a2, a3);
    return instance;
  } else {
    returnnew Klass(a1, a2, a3); }};Copy the code

Look at this, we want the answer is clear. GetPooled simply pops an instance object from the pooled class instance pool (which is an array) and re-initializes it. This is the following two lines of code:

var instance = Klass.instancePool.pop();
Klass.call(instance, a1, a2, a3);
Copy the code

At this point, I don’t know if you understand? For the Event Object, we put it back into the instance pool at the end of the Event loop: klass.instancepool. Push (instance); . Take it out again at the start of the next event loop :var instance = klass.instancepool. .

The four stages of sorting and explaining have been completed. So let’s make a quick summary.

conclusion

I gained a lot of profound knowledge by constantly clarifying research points, and then repeatedly writing code, debugging and verification. It is these profound understandings that enable me to unveil the mysterious veil of react synthetic event system and clearly see its true face. At the same time, I’ve deepened my understanding of the native event mechanic.

Here’s what I learned:

  • Top level is actually a Document object;
  • Listen at top level listen at top level listen at top level listen at top level listen at top level listen at top level listen at top level
  • Many people may not know that the concept of “dispatch” means that all event listeners in the same event propagation path share the same event object in both the native and react synthetic event systems.
  • An event object is a composite of the scene, which is the trigger time relative to the native event.
  • Take a closer look at some of the more obscure usages (event.nativeEvent, event.persistent(), onXXXCapture for registering and capturing events).
  • Mixing the react event system with the react synthetic event system makes it easy to step into a pit. The event Listeners registered in the native event system and the Event Listeners registered in the React synthetic event system are in two different systems. We must be clear about this. We need to know that the bridge between the two systems is on the Document object. Knowing this, we can draw a conclusion:
    • In the react synthetic event system, we can’t stop to native events in the system (yes, the event. NativeEvent. StopPropagation () also not line).
    • However, in the native event system, we can prevent events from propagating to the React composite event system.

I’ve racked my brains and that’s all I’ve got. Although this article explores the composite event system for Replay V0.8.0, I believe that the main architecture and runtime principles of the composite event system have not changed much since the release to V16.12.0.

After the whole article, I believe that the general process is also clear, but some details are not in-depth. For example, the implementation details of the constructors of each synthesized event object, the pooling technical details, the transaction technical details, and so on. Just as the saying goes, the book is not full of words, I hope everyone to explore and explore. If you find your point of view wrong in the reading process, please do not hesitate to comment and correct.

Thank you for reading.