A Walkthrough of React's Event Mechanism Source Code

React has a sophisticated event mechanism. In a nutshell, in the web environment, React utilizes event delegation, making the document act as the event listener. When an event is triggered, it finds all bound event callback functions in the React component and executes them sequentially. So, how does React set up corresponding event listener functions on the document, and how does it execute various callback functions when events are triggered? This article will carefully go through the code of React's event mechanism on the web. (This article is based on React version 15.6.0)

The React source code includes an official diagram introducing React's event mechanism. Although the information in the diagram is too simple to provide a detailed understanding of React's event mechanism, it can still be useful to get a general idea:


/**
 * Summary of `ReactBrowserEventEmitter` event handling:
 *
 *  - Top-level delegation is used to trap most native browser events. This
 *    may only occur in the main thread and is the responsibility of
 *    ReactEventListener, which is injected and can therefore support pluggable
 *    event sources. This is the only work that occurs in the main thread.
 *
 *  - We normalize and de-duplicate events to account for browser quirks. This
 *    may be done in the worker thread.
 *
 *  - Forward these native events (with the associated top-level type used to
 *    trap it) to `EventPluginHub`, which in turn will ask plugins if they want
 *    to extract any synthetic events.
 *
 *  - The `EventPluginHub` will then process each event by annotating them with
 *    "dispatches", a sequence of listeners and IDs that care about that event.
 *
 *  - The `EventPluginHub` then dispatches the events.
 *
 * Overview of React and the event system:
 *
 * +------------+    .
 * |    DOM     |    .
 * +------------+    .
 *       |           .
 *       v           .
 * +------------+    .
 * | ReactEvent |    .
 * |  Listener  |    .
 * +------------+    .                         +-----------+
 *       |           .               +--------+|SimpleEvent|
 *       |           .               |         |Plugin     |
 * +-----|------+    .               v         +-----------+
 * |     |      |    .    +--------------+                    +------------+
 * |     +-----------.--->|EventPluginHub|                    |    Event   |
 * |            |    .    |              |     +-----------+  | Propagators|
 * | ReactEvent |    .    |              |     |TapEvent   |  |------------|
 * |  Emitter   |    .    |              |<---+|Plugin     |  |other plugin|
 * |            |    .    |              |     +-----------+  |  utilities |
 * |     +-----------.--->|              |                    +------------+
 * |     |      |    .    +--------------+
 * +-----|------+    .                ^        +-----------+
 *       |           .                |        |Enter/Leave|
 *       +           .                +-------+|Plugin     |
 * +-------------+   .                         +-----------+
 * | application |   .
 * |-------------|   .
 * |             |   .
 * |             |   .
 * +-------------+   .
 *                   .
 *    React Core     .  General Purpose Event Plugin System
 */

I divide the React event mechanism into three stages:

The first stage is the preparation stage for React's event mechanism. It involves registering the ReactBrowserEventEmitter module's ReactEventListener (which is responsible for React event listening). It also registers ComponentTree and TreeTraversal from EventPluginUtils, and generates various data for the EventPluginRegistry module (responsible for managing event plugins). This stage occurs when the React project import ReactDOM.

The second stage is the event registration stage, which primarily does two things: binding corresponding event callback functions to the document and storing React event callback functions in EventPluginHub. This stage occurs before mounting the React elements which have be bound event handling functions.

The third stage is the event triggering stage, which involves generating synthetic event objects and sequentially executing React event callback functions. This stage occurs after the browser event is triggered.

(Reading reminder: Due to the complexity of the React event mechanism code, including the complexity of the event mechanism itself, React's need to be compatible with both web and native platforms, the use of ES5 and CommonJS in the React source code, and the extensive use of tools wrapped in various design patterns, you may often find yourself suddenly losing track of the code you are reading and not knowing what the code you are reading is doing. Therefore, when reading the React source code, always read with a purpose and constantly remind yourself of what functionality the code you are currently reading serves. Also, it is advisable to understand commonly used tools and design patterns in the React source code, such as Transaction and PooledClass, before diving into the reading.)

I. React Event Mechanism - Preparation Stage

The preparation stage code is straightforward but crucial for understanding the React event mechanism.

The entry point for the preparation stage is in the src/renderers/dom/ReactDOM.js file. ReactDOM.js executes ReactDefaultInjection.inject() before exporting the ReactDOM object, initiating important preparations for the event mechanism. In other words, when ReactDom is introduced into the React project, the preparation work for the event mechanism is completed. The ReactDefaultInjection.inject function performs many tasks, and the event-related code is as follows (src/renderers/dom/shared/ReactDefaultInjection.js):

  ReactInjection.EventEmitter.injectReactEventListener(ReactEventListener);

  /**
   * Inject modules for resolving DOM hierarchy and plugin ordering.
   */
  ReactInjection.EventPluginHub.injectEventPluginOrder(DefaultEventPluginOrder);
  ReactInjection.EventPluginUtils.injectComponentTree(ReactDOMComponentTree);
  ReactInjection.EventPluginUtils.injectTreeTraversal(ReactDOMTreeTraversal);

  /**
   * Some important event plugins included by default (without having to require
   * them).
   */
  ReactInjection.EventPluginHub.injectEventPluginsByName({
    SimpleEventPlugin: SimpleEventPlugin,
    EnterLeaveEventPlugin: EnterLeaveEventPlugin,
    ChangeEventPlugin: ChangeEventPlugin,
    SelectEventPlugin: SelectEventPlugin,
    BeforeInputEventPlugin: BeforeInputEventPlugin,
  });

1. ReactInjection.EventEmitter.injectReactEventListener(ReactEventListener)

ReactInjection.EventEmitter is the ReactBrowerEventEmitter module (src/renders/dom/client/ReactBrowerEventEmitter.js). After executing injectReactEventListener(ReactEventListener), it saves ReactEventListener for later use in event listening. It also sets its own handleTopLevel function as the ReactEventListener's handleTopLevel function for use when events are triggered. The relevant code is as follows:

    injectReactEventListener: function(ReactEventListener) {
      ReactEventListener.setHandleTopLevel(
        ReactBrowserEventEmitter.handleTopLevel,
      );
      ReactBrowserEventEmitter.ReactEventListener = ReactEventListener;
    },

2. EventPluginUtils.injectComponentTree and EventPluginUtils.injectTreeTraversal

These two functions are EventPluginUtils module's injectComponentTree and injectTreeTraversal functions. They respectively save ReactDOMComponentTree and ReactDOMTreeTraversal as EventPluginUtils module's ComponentTree and TreeTraversal for later use in constructing synthetic event objects. The code is as follows (src/renderers/shared/stack/event/EventPluginUtils.js):

/**
 * Injected dependencies:
 */

/**
 * - `ComponentTree`: [required] Module that can convert between React instances
 *   and actual node references.
 */
var ComponentTree;
var TreeTraversal;
var injection = {
  injectComponentTree: function(Injected) {
    ComponentTree = Injected;
  },
  injectTreeTraversal: function(Injected) {
    TreeTraversal = Injected;
  },
};

3. EventPluginHub.injectEventPluginOrder and EventPluginHub.injectEventPluginsByName

The React event object is a synthetic event object, generated using various event plugins. EventPluginHub.injectEventPluginsByName and injectEventPluginOrder are actually calling functions with the same names from the EventPluginRegistry module. After execution, the SimpleEventPlugin, EnterLeaveEventPlugin, and other five event plugins are registered in the EventPluginRegistry's namesToPlugins object. Additionally, various data is generated as follows:

/**
 * Injectable ordering of event plugins.
 */
var eventPluginOrder: EventPluginOrder = null;  

/**
   * Ordered list of injected plugins.
   */
  plugins: [],

  /**
   * Mapping from event name to dispatch config
   */
  eventNameDispatchConfigs: {},

  /**
   * Mapping from registration name to plugin module
   */
  registrationNameModules: {},

  /**
   * Mapping from registration name to event name
   */
  registrationNameDependencies: {},

The result is that EventPluginRegistry can easily retrieve event plugins corresponding to events and their dependent events using information such as event names and registration names. The specific code is as follows (src/renders/shared/stack/event/EventPluginRegistry.js):

  injectEventPluginOrder: function(
    injectedEventPluginOrder: EventPluginOrder,
  ): void {
    eventPluginOrder = Array.prototype.slice.call(injectedEventPluginOrder);
    recomputePluginOrdering();
  },

  injectEventPluginsByName: function(
    injectedNamesToPlugins: NamesToPlugins,
  ): void {
    var isOrderingDirty = false;
    for (var pluginName in injectedNamesToPlugins) {
      if (!injectedNamesToPlugins.hasOwnProperty(pluginName)) {
        continue;
      }
      var pluginModule = injectedNamesToPlugins[pluginName];
      if (
        !namesToPlugins.hasOwnProperty(pluginName) ||
        namesToPlugins[pluginName] !== pluginModule
      ) {
        namesToPlugins[pluginName] = pluginModule;
        isOrderingDirty = true;
      }
    }
    if (isOrderingDirty) {
      recomputePluginOrdering();
    }
  },

To understand the publishEventForPlugin function and later the publishRegistrationName function, let's first look at the code for event plugins:

var eventTypes = {
  change: {
    phasedRegistrationNames: {
      bubbled: 'onChange',
      captured: 'onChangeCapture',
    },
    dependencies: [
      'topBlur',
      'topChange',
      'topClick',
      'topFocus',
      'topInput',
      'topKeyDown',
      'topKeyUp',
      'topSelectionChange',
    ],
  },
};

/**
 * This plugin creates an `onChange` event that normalizes change events
 * across form elements. This event fires at a time when it's possible to
 * change the element's value without seeing a flicker.
 *
 * Supported elements are:
 * - input (see `isTextInputElement`)
 * - textarea
 * - select
 */
var ChangeEventPlugin = {
  eventTypes: eventTypes,

  _allowSimulatedPassThrough: true,
  _isInputEventSupported: isInputEventSupported,

 // other codes
}

The recomputePluginOrdering function is as follows. It inserts plugins into the plugins function for each event plugin based on the eventPluginOrder and calls publishEventForPlugin:

/**
 * Publishes an event so that it can be dispatched by the supplied plugin.
 *
 * @param {object} dispatchConfig Dispatch configuration for the event.
 * @param {object} PluginModule Plugin publishing the event.
 * @return {boolean} True if the event was successfully published.
 * @private
 */
function recomputePluginOrdering(): void {
  if (!eventPluginOrder) {
    // Wait until an `eventPluginOrder` is injected.
    return;
  }
  for (var pluginName in namesToPlugins) {
    var pluginModule = namesToPlugins[pluginName];
    var pluginIndex = eventPluginOrder.indexOf(pluginName);
    if (EventPluginRegistry.plugins[pluginIndex]) {
      continue;
    }
    EventPluginRegistry.plugins[pluginIndex] = pluginModule;
    var publishedEvents = pluginModule.eventTypes;
    for (var eventName in publishedEvents) {
      invariant(
        publishEventForPlugin(
          publishedEvents[eventName],
          pluginModule,
          eventName,
        ),
        'EventPluginRegistry: Failed to publish event `%s` for plugin `%s`.',
        eventName,
        pluginName,
      );
    }
  }
}

The publishRegistrationName function is as follows. It generates registrationNameModules and registrationNameDependencies:

/**
 * Publishes a registration name that is used to identify dispatched events and
 * can be used with `EventPluginHub.putListener` to register listeners.
 *
 * @param {string} registrationName Registration name to add.
 * @param {object} PluginModule Plugin publishing the event.
 * @private
 */
function publishRegistrationName(
  registrationName: string,
  pluginModule: PluginModule<AnyNativeEvent>,
  eventName: string,
): void {
  EventPluginRegistry.registrationNameModules[registrationName] = pluginModule;
  EventPluginRegistry.registrationNameDependencies[registrationName] =
    pluginModule.eventTypes[eventName].dependencies;
}

So far, the EventPluginRegistry module has completed registration. EventPluginHub can easily call the EventPluginRegistry.getPluginModuleForEvent function to retrieve the event plugin corresponding to the event for generating synthetic event objects. This will be explained in more detail later.

II. React Event Mechanism - Event Registration

The event registration stage primarily does two things: binding corresponding event callback functions to the document and storing React event callback functions in EventPluginHub.

The entry file for event registration is src/renderers/dom/shared/ReactDOMComponent.js. Before mounting the dom-type element, React traverses the element's props and handles different types of props differently. If the propKey is a React event, it executes the enqueuePutListener method for event registration. The specific code is as follows:

// other codes
} else if (registrationNameModules.hasOwnProperty(propKey)) {
 if (nextProp) {
  enqueuePutListener(this, propKey, nextProp, transaction);
  } else if (lastProp) {
    deleteListener(this, propKey);
  }
} else if (isCustomComponent(this._tag, nextProps)) {
// other codes

Yes, registrationNameModules is the EventPluginRegistry's registrationNameModules generated in the preparation stage. Any event that is "registered" in it will undergo event registration. The code for enqueuePutListener is as follows:

function enqueuePutListener(inst, registrationName, listener, transaction) {
  if (transaction instanceof ReactServerRenderingTransaction) {
    return;
  }
  var containerInfo = inst._hostContainerInfo;
  var isDocumentFragment =
    containerInfo._node && containerInfo._node.nodeType === DOC_FRAGMENT_TYPE;
  var doc = isDocumentFragment
    ? containerInfo._node
    : containerInfo._ownerDocument;
  listenTo(registrationName, doc);
  transaction.getReactMountReady().enqueue(putListener, {
    inst: inst,
    registrationName: registrationName,
    listener: listener,
  });
}

function putListener() {
  var listenerToPut = this;
  EventPluginHub.putListener(
    listenerToPut.inst,
    listenerToPut.registrationName,
    listenerToPut.listener,
  );
}

The listenTo method is the ReactBrowserEventEmitter.listenTo method, which completes the first task of the event registration stage—binding corresponding event callback functions to the document. The putListener calls the EventPluginHub.putListener function to complete the second task—storing React event callback functions in EventPluginHub. Let's look at them separately:

The code for ReactBrowserEventEmitter.listenTo is as follows:

  /**
   * We listen for bubbled touch events on the document object.
   *
   * Firefox v8.01 (and possibly others) exhibited strange behavior when
   * mounting `onmousemove` events at some node that was not the document
   * element. The symptoms were that if your mouse is not moving over something
   * contained within that mount point (for example on the background) the
   * top-level listeners for `onmousemove` won't be called. However, if you
   * register the `mousemove` on the document object, then it will of course
   * catch all `mousemove`s. This along with iOS quirks, justifies restricting
   * top-level listeners to the document object only, at least for these
   * movement types of events and possibly all events.
   *
   * @see http://www.quirksmode.org/blog/archives/2010/09/click_event_del.html
   *
   * Also, `keyup`/`keypress`/`keydown` do not bubble to the window on IE, but
   * they bubble to document.
   *
   * @param {string} registrationName Name of listener (e.g. `onClick`).
   * @param {object} contentDocumentHandle Document which owns the container
   */
  listenTo: function(registrationName, contentDocumentHandle) {
    var mountAt = contentDocumentHandle;
    var isListening = getListeningForDocument(mountAt);
    var dependencies =
      EventPluginRegistry.registrationNameDependencies[registrationName];

    for (var i = 0; i < dependencies.length; i++) {
      var dependency = dependencies[i];
      if (
        !(isListening.hasOwnProperty(dependency) && isListening[dependency])
      ) {
        if (dependency === 'topWheel') {
          if (isEventSupported('wheel')) {
            ReactBrowserEventEmitter.ReactEventListener.trapBubbledEvent(
              'topWheel',
              'wheel',
              mountAt,
            );
          } else if (isEventSupported('mousewheel')) {
            ReactBrowserEventEmitter.ReactEventListener.trapBubbledEvent(
              'topWheel',
              'mousewheel',
              mountAt,
            );
          } else {
            // Firefox needs to capture a different mouse scroll event.
            // @see http://www.quirksmode.org/dom/events/tests/scroll.html
            ReactBrowserEventEmitter.ReactEventListener.trapBubbledEvent(
              'topWheel',
              'DOMMouseScroll',
              mountAt,
            );
          }
        } else if (dependency === 'topScroll') {
          if (isEventSupported('scroll', true)) {
            ReactBrowserEventEmitter.ReactEventListener.trapCapturedEvent(
              'topScroll',
              'scroll',
              mountAt,
            );
          } else {
            ReactBrowserEventEmitter.ReactEventListener.trapBubbledEvent(
              'topScroll',
              'scroll',
              ReactBrowserEventEmitter.ReactEventListener.WINDOW_HANDLE,
            );
          }
        } else if (dependency === 'topFocus' || dependency === 'topBlur') {
          if (isEventSupported('focus', true)) {
            ReactBrowserEventEmitter.ReactEventListener.trapCapturedEvent(
              'topFocus',
              'focus',
              mountAt,
            );
            ReactBrowserEventEmitter.ReactEventListener.trapCapturedEvent(
              '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
            ReactBrowserEventEmitter.ReactEventListener.trapBubbledEvent(
              'topFocus',
              'focusin',
              mountAt,
            );
            ReactBrowserEventEmitter.ReactEventListener.trapBubbledEvent(
              'topBlur',
              'focusout',
              mountAt,
            );
          }

          // to make sure blur and focus event listeners are only attached once
          isListening.topBlur = true;
          isListening.topFocus = true;
        } else if (topEventMapping.hasOwnProperty(dependency)) {
          ReactBrowserEventEmitter.ReactEventListener.trapBubbledEvent(
            dependency,
            topEventMapping[dependency],
            mountAt,
          );
        }

        isListening[dependency] = true;
      }
    }
  },

The listenTo method first uses the registrationName to obtain the corresponding top event name through EventPluginRegistry.registrationNameDependencies. Here, registrationName is the event name bound to a React DOM element, such as onClick. The top event name is a marker name for events that can be bound to the document in React, such as topClick. By using the top event name, you can find the actual event name to be bound to the document, such as click. To clarify, when encountering an onClick event, it first identifies topClick and then determines the actual event to be bound to the document, which is click.

The specific binding method involves executing ReactBrowserEventEmitter.ReactEventListener.trapBubbledEvent (yes, this ReactEventListener is the one registered during the preparation phase).

  /**
   * Traps top-level events by using event bubbling.
   *
   * @param {string} topLevelType Record from `EventConstants`.
   * @param {string} handlerBaseName Event name (e.g. "click").
   * @param {object} element Element on which to attach listener.
   * @return {?object} An object with a remove function which will forcefully
   *                  remove the listener.
   * @internal
   */
  trapBubbledEvent: function(topLevelType, handlerBaseName, element) {
    if (!element) {
      return null;
    }
    return EventListener.listen(
      element,
      handlerBaseName,
      ReactEventListener.dispatchEvent.bind(null, topLevelType),
    );
  },

EventListener.listen is compatible with different browsers and sets specific event listening callback functions on the document to ReactEventListener.dispatchEvent.bind(null, topLevelType). The code for EventListener.listen is as follows:

  /**
   * Listen to DOM events during the bubble phase.
   *
   * @param {DOMEventTarget} target DOM element to register listener on.
   * @param {string} eventType Event type, e.g. 'click' or 'mouseover'.
   * @param {function} callback Callback function.
   * @return {object} Object with a `remove` method.
   */
  listen: function listen(target, eventType, callback) {
    if (target.addEventListener) {
      target.addEventListener(eventType, callback, false);
      return {
        remove: function remove() {
          target.removeEventListener(eventType, callback, false);
        }
      };
    } else if (target.attachEvent) {
      target.attachEvent('on' + eventType, callback);
      return {
        remove: function remove() {
          target.detachEvent('on' + eventType, callback);
        }
      };
    }
  },

So far, the event registration stage has completed the binding of corresponding event callback functions to the document. Now, for every React event, a corresponding event callback function ReactEventListener.dispatchEvent.bind(null, topLevelType) is bound to the document. As you can see, all events trigger the ReactEventListener.dispatchEvent function, differing only in the topLevelType parameter. As for how dispatchEvent executes the specific React event callback functions, let's slowly go through it.

To help you connect the dots, let's recall that the start of the event registration stage is before mounting the dom-type element in React, traversing the element's props and handling different types of props differently. For React event types, the ReactBrowserEventEmitter.listenTo method is executed to bind corresponding event callback functions to the document, as explained above. The other part is calling the EventPluginHub.putListener function to complete the second task—storing React event callback functions in EventPluginHub. The code for EventPluginHub.putListener is as follows:

  /**
   * Stores `listener` at `listenerBank[registrationName][key]`. Is idempotent.
   *
   * @param {object} inst The instance, which is the source of events.
   * @param {string} registrationName Name of listener (e.g. `onClick`).
   * @param {function} listener The callback to store.
   */
  putListener: function(inst, registrationName, listener) {
    var key = getDictionaryKey(inst);
    var bankForRegistrationName =
      listenerBank[registrationName] || (listenerBank[registrationName] = {});
    bankForRegistrationName[key] = listener;

    var PluginModule =
      EventPluginRegistry.registrationNameModules[registrationName];
    if (PluginModule && PluginModule.didPutListener) {
      PluginModule.didPutListener(inst, registrationName, listener);
    }
  },

This logic is relatively simple. The preceding code stores the event callback functions in the listenerBank object based on event name -> component instance -> callback function hierarchy. The subsequent code handles some special logic when storing callback functions for certain events, which can be ignored for now.

III. React Event Mechanism - Event Triggering

The event triggering process is essentially about how code is executed after an event occurs. In the earlier discussion of the event registration stage, for every React event, a corresponding ReactEventListener.dispatchEvent.bind(null, topLevelType) function is already bound to the document, and the event's callback functions are stored in EventPluginHub's listenerBank. So, when an event occurs, it triggers the ReactEventListener.dispatchEvent function. The code is as follows (src/renderers/dom/client/ReactEventListener.js):


  dispatchEvent: function(topLevelType, nativeEvent) {
    if (!ReactEventListener._enabled) {
      return;
    }

    var bookKeeping = TopLevelCallbackBookKeeping.getPooled(
      topLevelType,
      nativeEvent,
    );
    try {
      // Event queue being processed in the same cycle allows
      // `preventDefault`.
      ReactUpdates.batchedUpdates(handleTopLevelImpl, bookKeeping);
    } finally {
      TopLevelCallbackBookKeeping.release(bookKeeping);
    }
  },

TopLevelCallbackBookKeeping.getPooled generates an instance of TopLevelCallbackBookKeeping (not elaborated here; students unfamiliar with PooledClass in React may want to learn about it). ReactUpdates.batchedUpdates(handleTopLevelImpl, bookKeeping) sets the currently updating flag to true, entering the batch update state. It then executes handleTopLevelImpl(bookKeeping), and finally, it batch updates all components whose states have changed. Therefore, regardless of how many setState calls are made during an event, the UI updates only once. But this article focuses on explaining the event mechanism, so we are only interested in how handleTopLevelImpl(bookKeeping) calls the saved event callback functions. Let's look at the code for handleTopLevelImpl:


function handleTopLevelImpl(bookKeeping) {
  var nativeEventTarget = getEventTarget(bookKeeping.nativeEvent);
  var targetInst = ReactDOMComponentTree.getClosestInstanceFromNode(
    nativeEventTarget,
  );

  // Loop through the hierarchy, in case there's any nested components.
  // It's important that we build the array of ancestors before calling any
  // event handlers, because event handlers can modify the DOM, leading to
  // inconsistencies with ReactMount's node cache. See #1105.
  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];
    ReactEventListener._handleTopLevel(
      bookKeeping.topLevelType,
      targetInst,
      bookKeeping.nativeEvent,
      getEventTarget(bookKeeping.nativeEvent),
    );
  }
}

handleTopLevelImpl first finds the DOM element based on the native event object, then finds the corresponding reactDOMComponent instance through the DOM element. Finally, it gets the ancestor components and pushes them into the ancestors array in the bookKeeping. The term "ancestor" might be misleading—ancestor does not refer to the parent instance of the current element. The code to obtain ancestors is as follows:

/**
 * Find the deepest React component completely containing the root of the
 * passed-in instance (for use when entire React trees are nested within each
 * other). If React trees are not nested, returns null.
 */
function findParent(inst) {
  // TODO: It may be a good idea to cache this to prevent unnecessary DOM
  // traversal, but caching is difficult to do correctly without using a
  // mutation observer to listen for all DOM changes.
  while (inst._hostParent) {
    inst = inst._hostParent;
  }
  var rootNode = ReactDOMComponentTree.getNodeFromInstance(inst);
  var container = rootNode.parentNode;
  return ReactDOMComponentTree.getClosestInstanceFromNode(container);
}

In other words, if the current React component tree is mounted under another React component tree, it returns the ReactDOMComponent instance corresponding to the mounting element of the parent component tree. So, in most cases, the bookKeeping.ancestors array contains only one element—the reactDOMComponent instance corresponding to the DOM element where the event was triggered.

Then, for each reactDOMComponent instance in bookKeeping.ancestors, it executes ReactEventListener._handleTopLevel, where _handleTopLevel was injected as ReactEventEmitterMixin.handleTopLevel in the preparation stage. The code is as follows (src/renderers/shared/stack/reconciler/ReactEventEmitterMixin.js):

  /**
   * Streams a fired top-level event to `EventPluginHub` where plugins have the
   * opportunity to create `ReactEvent`s to be dispatched.
   */
  handleTopLevel: function(
    topLevelType,
    targetInst,
    nativeEvent,
    nativeEventTarget,
  ) {
    var events = EventPluginHub.extractEvents(
      topLevelType,
      targetInst,
      nativeEvent,
      nativeEventTarget,
    );
    runEventQueueInBatch(events);
  },

handleTopLevel first executes EventPluginHub.extractEvents to generate a synthetic event object, and then executes runEventQueueInBatch, which executes all React event callback functions.

Let's revisit the code to generate synthetic event objects:

  /**
   * Allows registered plugins an opportunity to extract events from top-level
   * native browser events.
   *
   * @return {*} An accumulation of synthetic events.
   * @internal
   */
  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) {
        var extractedEvents = possiblePlugin.extractEvents(
          topLevelType,
          targetInst,
          nativeEvent,
          nativeEventTarget,
        );
        if (extractedEvents) {
          events = accumulateInto(events, extractedEvents);
        }
      }
    }
    return events;
  },

For each event plugin in EventPluginRegistry.plugins, it retrieves the corresponding event plugin's extractEvents method to construct a synthetic event object. If successful, the synthetic event object is added to the events array. Here is an example using SimpleEventPlugin to illustrate the extractEvents method (src/renderers/dom/client/eventPlugins/SimpleEventPlugin.js):

  extractEvents: function(
    topLevelType: TopLevelTypes,
    targetInst: ReactInstance,
    nativeEvent: MouseEvent,
    nativeEventTarget: EventTarget,
  ): null | ReactSyntheticEvent {
    var dispatchConfig = topLevelEventsToDispatchConfig[topLevelType];
    if (!dispatchConfig) {
      return null;
    }
    var EventConstructor;
    switch (topLevelType) {
      case 'topAbort':
      case 'topCanPlay':
      case 'topCanPlayThrough':
      case 'topDurationChange':
      case 'topEmptied':
      case 'topEncrypted':
      case 'topEnded':
      case 'topError':
      case 'topInput':
      case 'topInvalid':
      case 'topLoad':
      case 'topLoadedData':
      case 'topLoadedMetadata':
      case 'topLoadStart':
      case 'topPause':
      case 'topPlay':
      case 'topPlaying':
      case 'topProgress':
      case 'topRateChange':
      case 'topReset':
      case 'topSeeked':
      case 'topSeeking':
      case 'topStalled':
      case 'topSubmit':
      case 'topSuspend':
      case 'topTimeUpdate':
      case 'topVolumeChange':
      case 'topWaiting':
        // HTML Events
        // @see http://www.w3.org/TR/html5/index.html#events-0
        EventConstructor = SyntheticEvent;
        break;
      case 'topKeyPress':
        // Firefox creates a keypress event for function keys too. This removes
        // the unwanted keypress events. Enter is however both printable and
        // non-printable. One would expect Tab to be as well (but it isn't).
        if (getEventCharCode(nativeEvent) === 0) {
          return null;
        }
      /* falls through */
      case 'topKeyDown':
      case 'topKeyUp':
        EventConstructor = SyntheticKeyboardEvent;
        break;
      case 'topBlur':
      case 'topFocus':
        EventConstructor = SyntheticFocusEvent;
        break;
      case '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 'topDoubleClick':
      case 'topMouseDown':
      case 'topMouseMove':
      case 'topMouseUp':
      // TODO: Disabled elements should not respond to mouse events
      /* falls through */
      case 'topMouseOut':
      case 'topMouseOver':
      case 'topContextMenu':
        EventConstructor = SyntheticMouseEvent;
        break;
      case 'topDrag':
      case 'topDragEnd':
      case 'topDragEnter':
      case 'topDragExit':
      case 'topDragLeave':
      case 'topDragOver':
      case 'topDragStart':
      case 'topDrop':
        EventConstructor = SyntheticDragEvent;
        break;
      case 'topTouchCancel':
      case 'topTouchEnd':
      case 'topTouchMove':
      case 'topTouchStart':
        EventConstructor = SyntheticTouchEvent;
        break;
      case 'topAnimationEnd':
      case 'topAnimationIteration':
      case 'topAnimationStart':
        EventConstructor = SyntheticAnimationEvent;
        break;
      case 'topTransitionEnd':
        EventConstructor = SyntheticTransitionEvent;
        break;
      case 'topScroll':
        EventConstructor = SyntheticUIEvent;
        break;
      case 'topWheel':
        EventConstructor = SyntheticWheelEvent;
        break;
      case 'topCopy':
      case 'topCut':
      case 'topPaste':
        EventConstructor = SyntheticClipboardEvent;
        break;
    }
    invariant(
      EventConstructor,
      'SimpleEventPlugin: Unhandled event type, `%s`.',
      topLevelType,
    );
    var event = EventConstructor.getPooled(
      dispatchConfig,
      targetInst,
      nativeEvent,
      nativeEventTarget,
    );
    EventPropagators.accumulateTwoPhaseDispatches(event);
    return event;
  },

The main idea is to select the corresponding synthetic event type based on topLevelType and generate a synthetic event object. Then, it executes EventPropagators.accumulateTwoPhaseDispatches(event) to find all reactDOM instances listening to this event and puts them into the _dispatchInstances of the synthetic event object. It also puts the callback functions corresponding to each instance into the _dispatchListeners of the synthetic event object. The code is as follows (src/renderers/shared/stack/event/EventPropagators.js):

function accumulateTwoPhaseDispatches(events) {
  forEachAccumulated(events, accumulateTwoPhaseDispatchesSingle);
}

forEachAccumulated(events, accumulateTwoPhaseDispatchesSingle) is essentially iterating over each synthetic event object event in the events array and executing accumulateTwoPhaseDispatchesSingle(event). The code for accumulateTwoPhaseDispatchesSingle is as follows:

/**
 * 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 cannot perform a
 * single traversal for the entire collection of events because each event may
 * have a different target.
 */
function accumulateTwoPhaseDispatchesSingle(event) {
  if (event && event.dispatchConfig.phasedRegistrationNames) {
    EventPluginUtils.traverseTwoPhase(
      event._targetInst,
      accumulateDirectionalDispatches,
      event,
    );
  }
}

The EventPluginUtils.traverseTwoPhase method executed here is the traverseTwoPhase method prepared in the preparation stage and registered in EventPluginUtils from ReactDOMTreeTraversal. The code is as follows (src/renderers/dom/client/ReactDOMTreeTraversal.js):

/**
 * Simulates the traversal of a two-phase, capture/bubble event dispatch.
 */
function traverseTwoPhase(inst, fn, arg) {
  var path = [];
  while (inst) {
    path.push(inst);
    inst = inst._hostParent;
  }
  var i;
  for (i = path.length; i-- > 0; ) {
    fn(path[i], 'captured', arg);
  }
  for (i = 0; i < path.length; i++) {
    fn(path[i], 'bubbled', arg);
  }
}

traverseTwoPhase first pushes all parent element instances of the current instance into the path array. Then, it simulates the event capture phase by starting to traverse the path array from the top-level instance and executing the accumulateDirectionalDispatches function (simulate the event capture phase). Then, it starts traversing the path array from the lower-level instance and executes the accumulateDirectionalDispatches function (simulate the event bubble phase). The code for the accumulateDirectionalDispatches function is as follows:

/**
 * 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's members allows us to not have to create a wrapping
 * "dispatch" object that pairs the event with the listener.
 */
function accumulateDirectionalDispatches(inst, phase, event) {

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

/**
 * Some event types have a notion of different registration names for different
 * "phases" of propagation. This finds listeners by a given phase.
 */
function listenerAtPhase(inst, event, propagationPhase: PropagationPhases) {
  var registrationName =
    event.dispatchConfig.phasedRegistrationNames[propagationPhase];
  return getListener(inst, registrationName);
}

Remember how the event callback functions were stored in EventPluginHub earlier? accumulateDirectionalDispatches first uses the getListener method to retrieve the event callback function bound to the corresponding element from EventPluginHub based on the event name and react dom instance. The getListener is the EventPluginHub's getListener method:

  /**
   * @param {object} inst The instance, which is the source of events.
   * @param {string} registrationName Name of listener (e.g. `onClick`).
   * @return {?function} The stored callback.
   */
  getListener: function(inst, registrationName) {
    // TODO: shouldPreventMouseEvent is DOM-specific and definitely should not
    // live here; needs to be moved to a better place soon
    var bankForRegistrationName = listenerBank[registrationName];
    if (
      shouldPreventMouseEvent(
        registrationName,
        inst._currentElement.type,
        inst._currentElement.props,
      )
    ) {
      return null;
    }
    var key = getDictionaryKey(inst);
    return bankForRegistrationName && bankForRegistrationName[key];
  },

If there is an event callback function, it puts the corresponding reactDOM instance into the _dispatchInstances of the synthetic event object and puts the callback function into the _dispatchListeners of the synthetic event object.

In this way, after executing accumulateTwoPhaseDispatchesSingle(event), each synthetic event object contains an _dispatchListeners array that stores all callback functions for this event from the capture phase to the bubble phase. It also contains a _dispatchInstances array that stores React DOM component instances corresponding to the callback functions in _dispatchListeners.

At this point, the construction of the synthetic event object is complete. If it feels confusing, go through the EventPluginHub.extractEvents function a few more times.

Before discussing the construction of the synthetic event object, we mentioned the ReactEventEmitterMixin.handleTopLevel function. It first executes EventPluginHub.extractEvents to build the synthetic event object, and then runs all callback functions through runEventQueueInBatch(events). Now, let's look at the code for runEventQueueInBatch:

function runEventQueueInBatch(events) {
  EventPluginHub.enqueueEvents(events);
  EventPluginHub.processEventQueue(false);
}

EventPluginHub.enqueueEvents adds all synthetic event objects to the eventQueue of EventPluginHub.

  /**
   * Enqueues a synthetic event that should be dispatched when
   * `processEventQueue` is invoked.
   *
   * @param {*} events An accumulation of synthetic events.
   * @internal
   */
  enqueueEvents: function(events) {
    if (events) {
      eventQueue = accumulateInto(eventQueue, events);
    }
  },

EventPluginHub.processEventQueue executes executeDispatchesAndReleaseTopLevel(event) for each synthetic event object event:

  /**
   * Dispatches all synthetic events on the event queue.
   *
   * @internal
   */
  processEventQueue: function(simulated) {
    // Set `eventQueue` to null before processing it so that we can tell if more
    // events get enqueued while processing.
    var processingEventQueue = eventQueue;
    eventQueue = null;
    if (simulated) {
      forEachAccumulated(
        processingEventQueue,
        executeDispatchesAndReleaseSimulated,
      );
    } else {
      forEachAccumulated(
        processingEventQueue,
        executeDispatchesAndReleaseTopLevel,
      );
    }

    // This would be a good time to rethrow if any of the event handlers threw.
    ReactErrorUtils.rethrowCaughtError();
  },

executeDispatchesAndReleaseTopLevel specifically calls methods from EventPluginUtils in sequence to execute the callback functions in the synthetic event object:

var executeDispatchesAndReleaseTopLevel = function(e) {
  return executeDispatchesAndRelease(e, false);
};
/**
 * Dispatches an event and releases it back into the pool, unless persistent.
 *
 * @param {?object} event Synthetic event to be dispatched.
 * @param {boolean} simulated If the event is simulated (changes exn behavior)
 * @private
 */
var executeDispatchesAndRelease = function(event, simulated) {
  if (event) {
    EventPluginUtils.executeDispatchesInOrder(event, simulated);

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

EventPluginUtils.executeDispatchesInOrder code is as follows:

/**
 * Standard/simple iteration through an event's collected dispatches.
 */
function executeDispatchesInOrder(event, simulated) {
  var dispatchListeners = event._dispatchListeners;
  var dispatchInstances = event._dispatchInstances;

  if (Array.isArray(dispatchListeners)) {
    for (var i = 0; i < dispatchListeners.length; i++) {
      if (event.isPropagationStopped()) {
        break;
      }
      // Listeners and Instances are two parallel arrays that are always in sync.
      executeDispatch(
        event,
        simulated,
        dispatchListeners[i],
        dispatchInstances[i],
      );
    }
  } else if (dispatchListeners) {
    executeDispatch(event, simulated, dispatchListeners, dispatchInstances);
  }
  event._dispatchListeners = null;
  event._dispatchInstances = null;
}

/**
 * Dispatch the event to the listener.
 * @param {SyntheticEvent} event SyntheticEvent to handle
 * @param {boolean} simulated If the event is simulated (changes exn behavior)
 * @param {function} listener Application-level callback
 * @param {*} inst Internal component instance
 */
function executeDispatch(event, simulated, listener, inst) {
  var type = event.type || 'unknown-event';
  event.currentTarget = EventPluginUtils.getNodeFromInstance(inst);
  if (simulated) {
    ReactErrorUtils.invokeGuardedCallbackWithCatch(type, listener, event);
  } else {
    ReactErrorUtils.invokeGuardedCallback(type, listener, event);
  }
  event.currentTarget = null;
}

For each event callback function in the _dispatchListeners of the synthetic event object, execute it through the executeDispatch function. executeDispatch mainly sets the currentTarget property of the synthetic event object to the node element corresponding to the element when the callback function is executed. It uses the ReactErrorUtils.invokeGuardedCallback method to execute the callback function to capture errors conveniently.

Thus, after all callback functions are executed, the event triggering phase is complete. To summarize the event triggering process:

  1. dispatchEvent generates bookKeeping and calls handleTopLevelImpl.
  2. handleTopLevelImpl finds all ancestor component instances and calls ReactEventListener._handleTopLevel, i.e., ReactEventEmitterMixin.handleTopLevel.
  3. handleTopLevel first executes EventPluginHub.extractEvents to generate the synthetic event object and then executes runEventQueueInBatch, running all React event callback functions.
  4. The process of generating the synthetic object is as follows: first, call the event plugin corresponding to the event to generate the synthetic event object. Then, traverse the parent component instances of the event-triggering element, find all event callback functions, and store them in the _dispatchListeners array of the synthetic event object.
  5. The process of executing callback functions is as follows: first, add all synthetic event objects to the eventQueue of EventPluginHub. Then, sequentially execute the stored callback functions in the synthetic event objects.

Now that we have covered all the code for the React event mechanism, I feel that the code for the React event mechanism is quite complex. When learning, I recommend understanding the code for each stage as I divided it into three stages before moving on to the next stage. Be patient and take it step by step.

Due to my limited expertise, there may be errors in the article. Corrections and constructive criticism are welcome.