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:
- dispatchEvent generates bookKeeping and calls handleTopLevelImpl.
- handleTopLevelImpl finds all ancestor component instances and calls ReactEventListener._handleTopLevel, i.e., ReactEventEmitterMixin.handleTopLevel.
- handleTopLevel first executes EventPluginHub.extractEvents to generate the synthetic event object and then executes runEventQueueInBatch, running all React event callback functions.
- 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.
- 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.