“This is the 8th day of my participation in the Gwen Challenge in November. Check out the details: The Last Gwen Challenge in 2021”
React version: V17.0.3
Synthetic event thought
React has its own event system pattern, often referred to as React composite events. Are using this kind of custom synthesis, on the one hand, in order to smooth the browser differences, make the React developers no longer need to focus on the browser compatibility issues, on the other hand is to unified management, improve performance, this is mainly embodied in the React internal implementation event delegation, and record the current state of the event.
Event delegate, also known as event broker, does not bind event handlers directly to real nodes. Instead, it binds all events to the outermost layer of the structure, using a unified event listener and handler. When a component is loaded or unloaded, objects are simply inserted or removed from the unified event listener. When an event occurs, it is first handled by the unified event listener, and then the actual event handler is found in the mapping table and called. This simplifies the event handling and recycling mechanisms and provides a significant efficiency boost.
This allows React to process the priority of different events. The highest priority is processed first. This also implements the incremental rendering idea of React, preventing frames from dropping and improving the user experience.
So that’s the idea of React synthesizing events.
Changes to event delegate
In Act17, React no longer attaches an event handler to the document. Instead, attach the event handler to the render React tree’s root DOM container:
const rootNode = document.getElementById('root');
ReactDOM.render(<App />, rootNode);
Copy the code
In React16 or earlier versions, the React will execute the document for most events. The addEventListener (). React17 will call in the underlying rootNode. AddEventListener ().
The image on the official website clearly shows the changes made to V17.0.0, which can be classified as event delegation, whether it is listening for events on the Document or the root DOM container.
Nonevent delegation
In React’s event architecture, not all events are implemented through event delegates, and some events are directly bound to target DOM elements. This part of the event is as follows:
// packages/react-dom/src/events/DOMPluginEventSystem.js // List of events that need to be individually attached to Media elements. // Export const mediaEventTypes: Array<DOMEventName> = [ 'abort', 'canplay', 'canplaythrough', 'durationchange', 'emptied', 'encrypted', 'ended', 'error', 'loadeddata', 'loadedmetadata', 'loadstart', 'pause', 'play', 'playing', 'progress', 'ratechange', 'resize', 'seeked', 'seeking', 'stalled', 'suspend', 'timeupdate', 'volumechange', 'waiting', ]; // We should not delegate these events to the container, but rather // set them on the actual target element itself. This is primarily // because these events do not Consistently bubble in the DOM. These events are not delegated to the root DOM container, but should be bound to the actual target element because they do not bubble in the DOM export const nonDelegatedEvents: Set<DOMEventName> = new Set([ 'cancel', 'close', 'invalid', 'load', 'scroll', 'toggle', // In order to reduce bytes, we insert the above array of media events // into this Set. Note: the "error" event isn't an exclusive media event, // and can occur on other elements too. Rather than duplicate that event, // we just take it from the media events array. ...mediaEventTypes, ]);Copy the code
The events in the source code above do not bubble in the DOM, so React binds them directly to the target DOM element.
Binding of events
React creates three global objects when a React application is started. The legacy mode after creating ReactDOMRoot objects in legacyCreateRootFromDOMContainer function call listenToAllSupportedEvents (rootContainerElement) The function binds events to the root DOM container (div#root). Concurrent mode is after creating ReactDOMRoot objects in createRoot function call listenToAllSupportedEvents (rootContainerElement) function in the root DOM container (div # root) To bind events.
// listen for events on the root DOM container (div#root) const rootContainerElement = container.nodetype === COMMENT_NODE? container.parentNode : container; listenToAllSupportedEvents(rootContainerElement);Copy the code
Visible, both boot mode to invoke the listenToAllSupportedEvents binding events. Now, let’s look at the implementation of this function.
listenToAllSupportedEvents
// packages/react-dom/src/events/DOMPluginEventSystem.js export function ListenToAllSupportedEvents (rootContainerElement: EventTarget) {/ / 1, the throttling optimization, to guarantee the global registration is called only once the if (! (rootContainerElement: any)[listeningMarker]) { (rootContainerElement: any)[listeningMarker] = true; // select * from allNativeEvents; ForEach (domEventName => {We handle Selectionchange (导 读) 导 读 because it doesn't Bubble and needs to be on the document. // SelectionChange does not bubble on the document, // Bind events other than selectionChange to the root DOM container if (domEventName! == 'selectionchange') { if (! Nondelegatedevents.has (domEventName)) {// nonDelegatedEvents stores events that don't bubble up in the DOM, such as events on video elements, // The second parameter to listenToNativeEvent is given false, ListenToNativeEvent (domEventName, False, rootContainerElement); } // Here we bind events that don't bubble up in the DOM to the target element // The second argument to listenToNativeEvent is passed true, ListenToNativeEvent (domEventName, True, rootContainerElement); }}); const ownerDocument = (rootContainerElement: any).nodeType === DOCUMENT_NODE ? rootContainerElement : (rootContainerElement: any).ownerDocument; if (ownerDocument ! == null) {// The selectionchange event also needs deduplication // but it is attached to The document Selectionchange event if (! (ownerDocument: any)[listeningMarker]) { (ownerDocument: any)[listeningMarker] = true; listenToNativeEvent('selectionchange', false, ownerDocument); }}}}Copy the code
In listenToAllSupportedEvents function:
-
Math.random generates a random string (listeningMarker) to ensure that the rootContainerElement global register is called only once, thus achieving throttling optimization.
-
Then the set collection allNativeEvents is traversed and event listeners are registered in the capture phase and the bubble phase respectively according to the characteristics of the event.
-
The nonDelegatedEvents variable stores events that don’t bubble up in the DOM, such as events on media elements (video tags, etc.), Scroll, Load, etc. These events don’t bubble up in the DOM, React registers them directly with the target DOM element.
-
For events that bubble up in the DOM, register them with the root DOM container (div#root).
// select * from allNativeEvents; ForEach (domEventName => {We handle Selectionchange (导 读) 导 读 because it doesn’t Bubble and needs to be on the document. // SelectionChange does not bubble on the document, // Bind events other than selectionChange to the root DOM container if (domEventName! == ‘selectionchange’) { if (! nonDelegatedEvents.has(domEventName)) {
// nonDelegatedEvents stores events that don't bubble up in the DOM, such as events on video elements, // The second parameter to listenToNativeEvent is given false, ListenToNativeEvent (domEventName, False, rootContainerElement); } // Here we bind events that don't bubble up in the DOM to the target element // The second argument to listenToNativeEvent is passed true, ListenToNativeEvent (domEventName, True, rootContainerElement);Copy the code
}});
-
Finally, the selectionChange event is processed separately, registering it with the target DOM element.
// set selectionChange if (! (ownerDocument: any)[listeningMarker]) { (ownerDocument: any)[listeningMarker] = true; listenToNativeEvent(‘selectionchange’, false, ownerDocument); }
Either registering event listeners in the capture phase or in the bubbling phase calls the listenToNativeEvent function, which we’ll look at next.
listenToNativeEvent
// packages/react-dom/src/events/DOMPluginEventSystem.js export function listenToNativeEvent( domEventName: DOMEventName, // Event name isCapturePhaseListener: Boolean, // Whether to listen for events in capture or bubble phase target: EventTarget, // Target event object): Void {// delete the Dev section of the code let eventSystemFlags = 0; If (isCapturePhaseListener) {/ / IS_CAPTURE_PHASE tag is in the capture phase eventSystemFlags | = IS_CAPTURE_PHASE; // Register event listener addTrappedEventListener(target, // target event object domEventName, // event name eventSystemFlags, // isCapturePhaseListener, // Boolean, false to listen for events in the bubble phase, true to listen for events in the capture phase); }Copy the code
The listenToNativeEvent function is not complicated. It adds an IS_CAPTURE_PHASE flag to the eventSystemFlags variable through bitoperations, marking that the event is in the capture phase. The addTrappedEventListener function is then called to register event listeners.
addTrappedEventListener
// packages/react-dom/src/events/DOMPluginEventSystem.js function addTrappedEventListener( targetContainer: DomEventName: domEventName, // Target event name eventSystemFlags: EventSystemFlags, isCapturePhaseListener: Boolean, / / false representation in the bubbling phase to monitor events, true isDeferredListenerForLegacyFBSupport said the capture phase listeners event? : Boolean,) {/ / 1, tectonic event listener let the listener = createEventListenerWrapperWithPriority (targetContainer domEventName, eventSystemFlags, ); // If passive option is not supported, // The event will be active and not passive. // The event will be active and not passive. // Some browsers (especially Chrome and Firefox) have called the document-level node Window, On the Document and the Document body touchstart defaults for the options and Passive touchmove events change to true if (passiveBrowserEventsSupported) {/ / Browsers introduced an intervention, making these events // passive by default on document. React doesn't bind them // to document anymore, but changing this now would undo // the performance wins from the change. So we emulate // the existing behavior manually on the roots now. // https://github.com/facebook/react/issues/19651 if ( domEventName === 'touchstart' || domEventName === 'touchmove' || domEventName === 'wheel' ) { isPassiveListener = true; }} // Is the root of the DOM container or target element targetContainer = enableLegacyFBSupport && isDeferredListenerForLegacyFBSupport? (targetContainer: any).ownerDocument : targetContainer; let unsubscribeListener; // When legacyFBSupport is enabled, it's for when we // want to add a one time event listener to a container. // This should only be used with enableLegacyFBSupport // due to requirement to provide compatibility with // internal FB www event tooling. This works by removing // the event listener as soon as it is invoked. We could // also attempt to use the {once: true} param on // addEventListener, but that requires support and some // browsers do not support this today, and given this is // to support legacy code patterns, it's likely they'll // need support for such browsers. if (enableLegacyFBSupport && isDeferredListenerForLegacyFBSupport) { const originalListener = listener; listener = function(... p) { removeEventListener( targetContainer, domEventName, unsubscribeListener, isCapturePhaseListener, ); return originalListener.apply(this, p); }; } // TODO: If (isCapturePhaseListener) {// If (isCapturePhaseListener) {// If (isCapturePhaseListener) {// If (isCapturePhaseListener) {// If (isCapturePhaseListener) {// If (isCapturePhaseListener) {// If (isCapturePhaseListener) Register event listener if (isPassiveListener! = = undefined) {/ / the event listener will be set to a passive event listener unsubscribeListener = addEventCaptureListenerWithPassiveFlag (targetContainer, domEventName, listener, isPassiveListener, ); } else {// The event listener will be set to a non-passive event listener unsubscribeListener = addEventCaptureListener(targetContainer, domEventName, listener, ); }} else {// Register event listener in bubble phase if (isPassiveListener! = = undefined) {/ / the event listener will be set to a passive event listener unsubscribeListener = addEventBubbleListenerWithPassiveFlag (targetContainer, domEventName, listener, isPassiveListener, ); } else {// The event listener is set to a non-passive event listener unsubscribeListener = addEventBubbleListener(targetContainer, domEventName, listener, ); }}}Copy the code
In the addTrappedEventListener function:
-
First call createEventListenerWrapperWithPriority function to construct the event listener listener.
/ / 1, tectonic event listener let the listener = createEventListenerWrapperWithPriority (targetContainer, domEventName eventSystemFlags,);
-
It then initializes the variable isPassiveListener, which is used to distinguish between passive and non-passive event listeners.
// set passivelistener = null; // Some browsers (especially Chrome and Firefox) have called the document-level node Window, On the Document and the Document body touchstart defaults for the options and Passive touchmove events change to true if (passiveBrowserEventsSupported) {/ / Browsers introduced an intervention, making these events // passive by default on document. React doesn’t bind them // to document anymore, but changing this now would undo // the performance wins from the change. So we emulate // the existing behavior Manually on the roots now. // github.com/facebook/re… if ( domEventName === ‘touchstart’ || domEventName === ‘touchmove’ || domEventName === ‘wheel’ ) { isPassiveListener = true; // Set to passive event listener}}
-
We then get the target object that the event is listening to, whether it’s the root DOM container or the target DOM element.
/ / 3, obtain the binding target container of events, is the root of the DOM container or target element targetContainer = enableLegacyFBSupport && isDeferredListenerForLegacyFBSupport? (targetContainer: any).ownerDocument : targetContainer;
-
Finally, event listeners are registered in the capture phase and the bubble phase, respectively, according to the isCapturePhaseListener parameter. When event listeners are registered in the capture and bubble phases, events are registered as passive event listeners and non-passive event listeners according to isPassiveListener.
If (isCapturePhaseListener) {if (isCapturePhaseListener) {
// Register event listeners in the capture phase
if (isPassiveListener ! = = undefined) {/ / the event listener will be set to a passive event listener unsubscribeListener = addEventCaptureListenerWithPassiveFlag (targetContainer, domEventName, listener, isPassiveListener, ); } else {// The event listener will be set to a non-passive event listener unsubscribeListener = addEventCaptureListener(targetContainer, domEventName, listener, ); } } else {
// Register event listeners during the bubbling phase
if (isPassiveListener ! = = undefined) {/ / the event listener will be set to a passive event listener unsubscribeListener = addEventBubbleListenerWithPassiveFlag (targetContainer, domEventName, listener, isPassiveListener, ); } else {// The event listener is set to a non-passive event listener unsubscribeListener = addEventBubbleListener(targetContainer, domEventName, listener, ); }}
When registering event listeners, Called separately AddEventCaptureListenerWithPassiveFlag, addEventCaptureListener, addEventBubbleListenerWithPassiveFlag, addEventBubbleListe Ner, let’s look at these functions.
Native Event Registration
/ / packages/react - dom/SRC/events/EventListener js / / in the bubbling phase Registered native events export function addEventBubbleListener (target: EventTarget, eventType: string, listener: Function, ): Function { // target.addEventListener(type, listener, useCapture); // useCapture is a Boolean, // True - The event handle executes in the capture phase // false- The event handle executes in the bubble phase target.addeventListener (eventType, Listener, false); return listener; } // Export function addEventCaptureListener(Target: EventTarget, eventType: string, listener: Function, ): Function { // target.addEventListener(type, listener, useCapture); // useCapture is a Boolean, // True - Event handle executes in capture phase // false- Event handle executes in bubble phase target.addEventListener(eventType, Listener, true); return listener; } / / in the capture phase passive event listener registered an export function addEventCaptureListenerWithPassiveFlag (target EventTarget, eventType: string, listener: Function, passive: boolean, ): Function { // target.addEventListener(type, listener, options); // options: Specifies an optional parameter object for the Listener property. The options are as follows: // capture: Boolean, indicating that the listener is triggered when an event capture of this type is propagated to the EventTarget. // once: Boolean, indicating that the listener is called at most once after being added. If true, the listener is automatically removed after it is called. // passive: Boolean, if true, the listener will never call preventDefault(). target.addEventListener(eventType, listener, { capture: true, passive, }); return listener; } / / in the bubbling phase passive event listener registered an export function addEventBubbleListenerWithPassiveFlag (target EventTarget, eventType: string, listener: Function, passive: boolean, ): Function { // target.addEventListener(type, listener, options); // options: Specifies an optional parameter object for the Listener property. The options are as follows: // capture: Boolean, indicating that the listener is triggered when an event capture of this type is propagated to the EventTarget. // once: Boolean, indicating that the listener is called at most once after being added. If true, the listener is automatically removed after it is called. // passive: Boolean, if true, the listener will never call preventDefault(). target.addEventListener(eventType, listener, { passive, }); return listener; }Copy the code
AddEventBubbleListener: Registers the event as a non-passive event listener during the bubbling phase.
AddEventCaptureListener: Registers the event as a non-passive event listener during the capture phase.
AddEventCaptureListenerWithPassiveFlag: in the capture phase, the event is registered as a passive event listeners.
AddEventBubbleListenerWithPassiveFlag: in the bubbling phase, the event is registered as a passive event listeners.
Each of these functions internally calls addEventListener to listen for native events, but with different arguments.
At this point, both native events registered on the root DOM container (div#root) and the target DOM element complete event listening.
The flow chart
A native listener
In event registration function addEventListener function need to pass in a event listeners the listener, the monitoring function is created by createEventListenerWrapperWithPriority function.
createEventListenerWrapperWithPriority
// packages/react-dom/src/events/ReactDOMEventListener.js export function createEventListenerWrapperWithPriority( targetContainer: EventTarget, domEventName: DOMEventName, eventSystemFlags: EventSystemFlags, ): Const eventPriority = getEventPriority(domEventName); let listenerWrapper; // Switch (eventPriority) {case DiscreteEventPriority: // The event has the highest priority. ListenerWrapper = dispatchDiscreteEvent; break; Case ContinuousEventPriority: // Moderate event priority listenerWrapper = dispatchContinuousEvent; break; Case DefaultEventPriority: // Event with the lowest priority Default: listenerWrapper = dispatchEvent; break; } // Return listenerWrapper. Bind (null, domEventName, eventSystemFlags, targetContainer,); }Copy the code
As you can see, different domEventNames call getEventPriority with different event priorities, resulting in three cases:
-
DiscreteEventPriority
-
Discrete events, such as Click, KeyDown, focusIn, etc., are not triggered consecutively and have the highest priority
-
The corresponding Listener is dispatchDiscreteEvent
-
ContinuousEventPriority
-
Block events such as Drag, mousemove, Scroll, etc. These events are characterized by continuous firing, block rendering, and have a moderate priority
-
The listener is dispatchContinuousEvent
-
DefaultEventPriority
-
Events, such as load and animation, have the lowest priority
-
The corresponding listener is dispatchEvent
Let’s look at the listener for each of these three cases.
dispatchDiscreteEvent
// packages/react-dom/src/events/ReactDOMEventListener.js function dispatchDiscreteEvent( domEventName, EventSystemFlags, container, nativeEvent,) {const previousPriority = getCurrentUpdatePriority(); const prevTransition = ReactCurrentBatchConfig.transition; ReactCurrentBatchConfig.transition = 0; Try {// Reset the update priority of the current update to DiscreteEventPriority(DiscreteEventPriority) setCurrentUpdatePriority(DiscreteEventPriority); dispatchEvent(domEventName, eventSystemFlags, container, nativeEvent); SetCurrentUpdatePriority (previousPriority);} finally {// Reset the update priority of the current update to previousPriority; ReactCurrentBatchConfig.transition = prevTransition; }}Copy the code
As you can see, the implementation of dispatchDiscreteEvent is not complicated, it does the following:
-
Get the update priority of the current update;
-
Reset the update priority of the current Update to DiscreteEventPriority (the highest priority of the event).
-
Then call dispatchEvent;
-
Update priority resets the current update priority to previousPriority
dispatchContinuousEvent
// packages/react-dom/src/events/ReactDOMEventListener.js function dispatchContinuousEvent( domEventName, EventSystemFlags, container, nativeEvent,) {const previousPriority = getCurrentUpdatePriority(); const prevTransition = ReactCurrentBatchConfig.transition; ReactCurrentBatchConfig.transition = 0; Try {// Reset the update priority of the current UPDATE to ContinuousEventPriority(mid-priority of the event) setCurrentUpdatePriority(ContinuousEventPriority); dispatchEvent(domEventName, eventSystemFlags, container, nativeEvent); SetCurrentUpdatePriority (previousPriority);} finally {// Reset the update priority of the current update to previousPriority; ReactCurrentBatchConfig.transition = prevTransition; }}Copy the code
As you can see, the implementation of dispatchContinuousEvent is not complicated, it does the following:
-
Get the update priority of the current update;
-
Reset the update priority of the current UPDATE to ContinuousEventPriority(medium priority of the event)
-
Then call dispatchEvent;
-
Update priority resets the current update priority to previousPriority
DispatchDiscreteEvent and dispatchContinuousEvent are actually packaging of dispatchEvent. Next, we will focus on this function.
Events trigger
When a native event is triggered, the dispatchEvent function is first entered. The dispatchEvent function is the most critical function in the React event system.
dispatchEvent
// packages/react-dom/src/events/ReactDOMEventListener.js export function dispatchEvent( domEventName: DOMEventName, eventSystemFlags: EventSystemFlags, targetContainer: EventTarget, nativeEvent: AnyNativeEvent, ): void { if (! _enabled) { return; } // TODO: replaying capture phase events is currently broken // because we used to do it during top-level native bubble handlers // but now we use different bubble and capture handlers. // In eager mode, we attach capture listeners early, so we need // to filter them out until we fix the logic to handle them correctly. const allowReplay = (eventSystemFlags & IS_CAPTURE_PHASE) === 0; if ( allowReplay && hasQueuedDiscreteEvents() && isDiscreteEventThatRequiresHydration(domEventName) ) { // If we already have a queue of discrete events, and this is another discrete // event, then we can't dispatch it regardless of its target, Since they // need to dispatch in order. // DiscreteEvents should be sent sequentially. // queueDiscreteEvent will create a replayable event, for example, click, keyDown, focusIn, etc. Add it to the "DiscreteEvent queue to replay" queueDiscreteEvent(null, // Flags that we're not actually blocked on anything as far as we know. domEventName, eventSystemFlags, targetContainer, nativeEvent, ); return; } // Try scheduling events, if blocked, Return SuspenseInstance or Container let blockedOn = attemptToDispatchEvent(domEventName, eventSystemFlags, targetContainer, nativeEvent, ); If (blockedOn === null) {// We successfully dispatched this event. If (allowReplay) {// Resets consecutive triggered events to no event clearIfContinuousEvent(domEventName, nativeEvent); } return; } if (allowReplay) { if ( ! enableCapturePhaseSelectiveHydrationWithoutDiscreteEventReplay && isDiscreteEventThatRequiresHydration(domEventName) ) { // This This to be replayed later once the target is available. // queueDiscreteEvent will create a replayable event, QueueDiscreteEvent (blockedOn, domEventName, eventSystemFlags, targetContainer, nativeEvent, ); return; } if ( queueIfContinuousEvent( blockedOn, domEventName, eventSystemFlags, targetContainer, nativeEvent, ) ) { return; } // We need to clear only if We didn't queue because // queueing is accumulative. // Reset events of the sequential trigger type to no events clearIfContinuousEvent(domEventName, nativeEvent); } if ( enableCapturePhaseSelectiveHydrationWithoutDiscreteEventReplay && eventSystemFlags & IS_CAPTURE_PHASE && isDiscreteEventThatRequiresHydration(domEventName) ) { while (blockedOn ! == null) { const fiber = getInstanceFromNode(blockedOn); if (fiber ! == null) { attemptSynchronousHydration(fiber); } // Try scheduling events, if blocked, Return SuspenseInstance or Container const nextBlockedOn = attemptToDispatchEvent(domEventName, eventSystemFlags, targetContainer, nativeEvent, ); if (nextBlockedOn === blockedOn) { break; } blockedOn = nextBlockedOn; } if (blockedOn) {// Implement the stopPropagation method of native events, To prevent further transmission event bubbling phase in capture phase (transmission means the bubbling up or down to the parent element capture to child elements) nativeEvent. StopPropagation (); return; } } // This is not replayable so we'll invoke it but without a target, // In case the event system needs to trace it. Distributed event dispatchEventForPluginEventSystem (domEventName, eventSystemFlags nativeEvent, null, targetContainer,); }Copy the code
DispatchEvent function implementation code is more, we focus on in the dispatchEvent function call attemptToDispatchEvent functions and dispatchEventForPluginEventSystem functions:
- AttemptToDispatchEvent the attemptToDispatchEvent function is an attempt to dispatch the event, and if the dispatch event fails, returns the SuspenseInstance or root DOM container.
- DispatchEventForPluginEventSystem function role, it is through the React of plug-in system to distribute events.
AttemptToDispatchEvent — Associate Fiber
// packages/react-dom/src/events/ReactDOMEventListener.js // Attempt dispatching an event. Returns a SuspenseInstance or Container if it's blocked. // Attempt to schedule events. If blocked, return SuspenseInstance or Container. export function attemptToDispatchEvent( domEventName: DOMEventName, eventSystemFlags: EventSystemFlags, targetContainer: EventTarget, nativeEvent: AnyNativeEvent, ): null | Container | SuspenseInstance { // TODO: // get the native DOM const nativeEventTarget = getEventTarget(nativeEvent); / / 2, obtain and native fiber node of the DOM corresponding let targetInst = getClosestInstanceFromNode (nativeEventTarget); // The fiber tree has been unmounted, the component of the SuspenseComponent type, the root node, the fiber node corresponding to the native DOM, the nearest fiber node and the fiber node corresponding to the native DOM, Set targetInst to null if (targetInst! Const nearestMounted = getNearestMountedFiber(targetInst); If (nearestMounted === null) {// This tree has been unmounted always. Dispatch without a target targetInst = null; } else {// Get the component type const tag = nearestMounted. Tag; If (tag === SuspenseComponent) {// The component type is SuspenseComponent const instance = getSuspenseInstanceFromFiber(nearestMounted); if (instance ! == null) { // Queue the event to be replayed later. Abort dispatching since we // don't want this event dispatched twice through the event system. // TODO: If this is the first discrete event in the queue. Schedule an increased // priority for this boundary. return instance; } // This shouldn't happen, something went wrong but to avoid blocking // the whole system, dispatch the event without a target. // TODO: Warn. targetInst = null; } else if (tag === HostRoot) {// Const root: FiberRoot = nearestMounted. StateNode; if (root.isDehydrated) { // If this happens during a replay something went wrong and it might block // the whole system. return getContainerFromFiber(nearestMounted); } targetInst = null; } else if (nearestMounted ! == targetInst) { // If we get an event (ex: img onload) before committing that // component's mount, ignore it for now (that is, treat it as if it was an // event on a non-React tree). We might also consider queueing events and // dispatching them after the mount. targetInst = null; }}} // Distributed event dispatchEventForPluginEventSystem (domEventName, eventSystemFlags nativeEvent, targetInst, targetContainer,); // We're not blocked on anything. return null; }Copy the code
AttemptToDispatchEvent the attemptToDispatchEvent function attempts to dispatch the event, and if the event fails, returns the SuspenseInstance or root DOM container. In attemptToDispatchEvent, three main things are done:
1. Locate the native DOM node that triggered the event
const nativeEventTarget = getEventTarget(nativeEvent);
Copy the code
2. Obtain the fiber node corresponding to the native DOM node
let targetInst = getClosestInstanceFromNode(nativeEventTarget);
Copy the code
In the process of obtaining the fiber node corresponding to the native DOM node, if the fiber tree has been uninstalled, the component is of the SuspenseComponent type, the component is the root node, the fiber node corresponding to the native DOM, and the nearest fiber node and the fiber node corresponding to the native DOM are not the same node, TargetInst will be reset to NULL in all four cases.
The native event is associated with the Fiber tree through the above two steps.
3. Distribute events through the event plug-in system
dispatchEventForPluginEventSystem(
domEventName,
eventSystemFlags,
nativeEvent,
targetInst,
targetContainer,
);
Copy the code
dispatchEventForPluginEventSystem
// packages/react-dom/src/events/DOMPluginEventSystem.js export function dispatchEventForPluginEventSystem( domEventName: DOMEventName, eventSystemFlags: EventSystemFlags, nativeEvent: AnyNativeEvent, targetInst: null | Fiber, targetContainer: EventTarget, ): void { let ancestorInst = targetInst; if ( (eventSystemFlags & IS_EVENT_HANDLE_NON_MANAGED_NODE) === 0 && (eventSystemFlags & IS_NON_DELEGATED) === 0 ) { const targetContainerNode = ((targetContainer: any): Node); // If we are using the legacy FB support flag, We // defer the event to the null with a one // time event listener so we can defer the event. Use an event listener if (enableLegacyFBSupport && / if our event flags match the required flags for entering // FB legacy mode and we are processing the "click" event, // then we can defer the event to the "document", to allow // for legacy FB support, // The expected behavior was to // match React < 16 behavior of delegated delegated to the doc The click event is registered on the document domEventName === 'click' && (eventSystemFlags & SHOULD_NOT_DEFER_CLICK_FOR_FB_SUPPORT_MODE) === 0 {/ / register event listeners on the document deferClickToDocumentForLegacyFBSupport (domEventName targetContainer); return; } if (targetInst ! == null) { // The below logic attempts to work out if we need to change // the target fiber to a different ancestor. We had similar logic // in the legacy event system, except the big difference between // systems is that the modern event system now has an event listener // attached to each React Root and React Portal Root. Together, // the DOM nodes representing these roots are the "rootContainer". // To figure out which ancestor instance we should use, we traverse // up the fiber tree from the target instance and attempt to find // root boundaries that match that of our current "rootContainer". // If we find that "rootContainer", we find the parent fiber // sub-tree for that root and make that our ancestor instance. let node = targetInst; MainLoop: while (true) {if (node === null) {return; } const nodeTag = node.tag; if (nodeTag === HostRoot || nodeTag === HostPortal) { let container = node.stateNode.containerInfo; if (isMatchingRootContainer(container, targetContainerNode)) { break; } if (nodeTag === HostPortal) { // The target is a portal, but it's not the rootContainer we're looking for. // Normally portals handle their own events all the way down to the root. // So we should be able to stop now. However, we don't know if this portal // was part of *our* root. let grandNode = node.return; while (grandNode ! == null) { const grandTag = grandNode.tag; if (grandTag === HostRoot || grandTag === HostPortal) { const grandContainer = grandNode.stateNode.containerInfo; if ( isMatchingRootContainer(grandContainer, targetContainerNode) ) { // This is the rootContainer we're looking for and we found it as // a parent of the Portal. That means we can ignore it because the // Portal will bubble through to us. return; } } grandNode = grandNode.return; } } // Now we need to find it's corresponding host fiber in the other // tree. To do this we can use getClosestInstanceFromNode, but we // need to validate that the fiber is a host instance, otherwise // we need to traverse up through the DOM till we find the correct // node that is from the other tree. while (container ! == null) { const parentNode = getClosestInstanceFromNode(container); if (parentNode === null) { return; } const parentTag = parentNode.tag; if (parentTag === HostComponent || parentTag === HostText) { node = ancestorInst = parentNode; continue mainLoop; } container = container.parentNode; } } node = node.return; BatchedUpdates (() => dispatchEventsForPlugins(domEventName, eventSystemFlags, nativeEvent, ancestorInst, targetContainer, ), ); }Copy the code
DispatchEventForPluginEventSystem function is through the plug-in system distributing events. In this function, the click event listener is registered with the Document for compatibility with versions below 16. Then, starting from the DOM node that currently triggered the event, traverse up the Fiber tree to find the root DOM container, and finally call dispatchEventsForPlugins to dispatch the event.
dispatchEventsForPlugins
// packages/react-dom/src/events/DOMPluginEventSystem.js function dispatchEventsForPlugins( domEventName: DOMEventName, eventSystemFlags: EventSystemFlags, nativeEvent: AnyNativeEvent, targetInst: null | Fiber, targetContainer: EventTarget, ): Const nativeEventTarget = getEventTarget(nativeEvent); Const dispatchQueue: dispatchQueue = []; // Collect extractEvents(dispatchQueue, domEventName, targetInst, nativeEvent, nativeEventTarget, eventSystemFlags, targetContainer, ); ProcessDispatchQueue (dispatchQueue, eventSystemFlags); }Copy the code
In the dispatchEventsForPlugins function, four things are done:
-
The getEventTarget function is first called to get the native DOM node that triggered the event.
-
The dispatchQueue queue is then defined to store all listeners.
-
The extractEvents method is then called to collect all listeners and store them in the dispatchQueue queue.
-
Finally, the processDispatchQueue function is called to perform event dispatch.
Next, let’s look at extractEvents, a function that collects listeners.
ExtractEvents — Collects a listener
// packages/react-dom/src/events/DOMPluginEventSystem.js // TODO: // SimpleEventPlugin is a basic function of the event system. Is the core of the event system / / other plugin is essentially polyfills, in the absence of these polyfill plug-ins can be allowed to publish the React to build SimpleEventPlugin. RegisterEvents (); EnterLeaveEventPlugin.registerEvents(); ChangeEventPlugin.registerEvents(); SelectEventPlugin.registerEvents(); BeforeInputEventPlugin.registerEvents(); function extractEvents( dispatchQueue: DispatchQueue, domEventName: DOMEventName, targetInst: null | Fiber, nativeEvent: AnyNativeEvent, nativeEventTarget: null | EventTarget, eventSystemFlags: EventSystemFlags, targetContainer: EventTarget, ) { // TODO: we should remove the concept of a "SimpleEventPlugin". // This is the basic functionality of the event system. All // the other plugins are essentially polyfills. So the plugin // should probably be inlined somewhere and have its logic // be core the to event system. This would potentially allow // us to ship builds of React without the polyfilled plugins // SimpleEventPlugin is the basic function of the event system. Is the core of the event system SimpleEventPlugin. ExtractEvents (dispatchQueue domEventName, targetInst, nativeEvent, nativeEventTarget, eventSystemFlags, targetContainer, ); const shouldProcessPolyfillPlugins = (eventSystemFlags & SHOULD_NOT_PROCESS_POLYFILL_EVENT_PLUGINS) === 0; // We don't process these events unless we are in the // event's native "bubble" phase, which means that we're // not in the capture phase. That's because we emulate // the capture phase here still. This is a trade-off, // because in an ideal world we would not emulate and use // the phases properly, like we do with the SimpleEvent // plugin. However, the plugins below either expect // emulation (EnterLeave) or use state localized to that // plugin (BeforeInput, Change, Select). The state in // these modules complicates things, as you'll essentially // get the case where the capture phase event might change // state, only for the following bubble event to come in // later and not trigger anything as the state now // invalidates the heuristics of the event plugin. We // could alter all these plugins to work in such ways, but // that might cause other unknown side-effects that we // can't foresee right now. if (shouldProcessPolyfillPlugins) {// The following types of plug-ins are essentially polyfills, In the absence of these polyfill plug-ins can be allowed to publish the React to build EnterLeaveEventPlugin. ExtractEvents (dispatchQueue domEventName, targetInst, nativeEvent, nativeEventTarget, eventSystemFlags, targetContainer, ); ChangeEventPlugin.extractEvents( dispatchQueue, domEventName, targetInst, nativeEvent, nativeEventTarget, eventSystemFlags, targetContainer, ); SelectEventPlugin.extractEvents( dispatchQueue, domEventName, targetInst, nativeEvent, nativeEventTarget, eventSystemFlags, targetContainer, ); BeforeInputEventPlugin.extractEvents( dispatchQueue, domEventName, targetInst, nativeEvent, nativeEventTarget, eventSystemFlags, targetContainer, ); }}Copy the code
In the extractEvents function of domplugineventSystem. js file, 5 event plug-ins of React are called to collect listeners. The SimpleEventPlugin is the core of React event system and provides the basic functions of React event system. The other four event plug-ins are polyfills in essence, allowing React builds to be published without these polyfills.
Next, let’s focus on the SimpleEventPlugin.
SimpleEventPlugin.extractEvents
// packages/react-dom/src/events/plugins/SimpleEventPlugin.js function extractEvents( dispatchQueue: DispatchQueue, domEventName: DOMEventName, targetInst: null | Fiber, nativeEvent: AnyNativeEvent, nativeEventTarget: null | EventTarget, eventSystemFlags: EventSystemFlags, targetContainer: EventTarget, ): void { const reactName = topLevelEventsToReactNames.get(domEventName); if (reactName === undefined) { return; } // Let SyntheticEventCtor = SyntheticEvent; // React event system event type let reactEventType: string = domEventName; switch (domEventName) { case 'keypress': // 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: any): KeyboardEvent)) === 0) { return; } /* falls through */ case 'keydown': case 'keyup': SyntheticEventCtor = SyntheticKeyboardEvent; // keyboard compositing event break; case 'focusin': reactEventType = 'focus'; SyntheticEventCtor = SyntheticFocusEvent; // focus synthesis event break; case 'focusout': reactEventType = 'blur'; SyntheticEventCtor = SyntheticFocusEvent; // focus synthesis event break; case 'beforeblur': case 'afterblur': SyntheticEventCtor = SyntheticFocusEvent; // focus synthesis event break; case 'click': // Firefox creates a click event on right mouse clicks. This removes the // unwanted click events. if (nativeEvent.button === 2) { return; } /* falls through */ case 'auxclick': case 'dblclick': case 'mousedown': case 'mousemove': case 'mouseup': // TODO: Disabled elements should not respond to mouse events /* falls through */ case 'mouseout': case 'mouseover': case 'contextmenu': SyntheticEventCtor = SyntheticMouseEvent; // mouse synthesizes the event break; case 'drag': case 'dragend': case 'dragenter': case 'dragexit': case 'dragleave': case 'dragover': case 'dragstart': case 'drop': SyntheticEventCtor = SyntheticDragEvent; // Drag the synthesized event break; case 'touchcancel': case 'touchend': case 'touchmove': case 'touchstart': SyntheticEventCtor = SyntheticTouchEvent; // Move the touch synthesis event break; case ANIMATION_END: case ANIMATION_ITERATION: case ANIMATION_START: SyntheticEventCtor = SyntheticAnimationEvent; // Animate the compositing event break; case TRANSITION_END: SyntheticEventCtor = SyntheticTransitionEvent; // Animate the compositing event break; case 'scroll': SyntheticEventCtor = SyntheticUIEvent; // roll the synthesis event break; case 'wheel': SyntheticEventCtor = SyntheticWheelEvent; // add event break; case 'copy': case 'cut': case 'paste': SyntheticEventCtor = SyntheticClipboardEvent; // Copy/paste/cut the synthesized event break; case 'gotpointercapture': case 'lostpointercapture': case 'pointercancel': case 'pointerdown': case 'pointermove': case 'pointerout': case 'pointerover': case 'pointerup': SyntheticEventCtor = SyntheticPointerEvent; break; default: // Unknown event. This is used by createEventHandle. break; } // Capture phase const inCapturePhase = (eventSystemFlags & IS_CAPTURE_PHASE)! = = 0; If (enableCreateEventHandleAPI && eventSystemFlags & IS_EVENT_HANDLE_NON_MANAGED_NODE) {/ / / / capture stage to collect all monitoring of the event listener const listeners = accumulateEventHandleNonManagedNodeListeners( // TODO: this cast may not make sense for events like // "focus" where React listens to e.g. "focusin". ((reactEventType: any): DOMEventName), targetContainer, inCapturePhase, ); If (listeners. Length > 0) {/ / Intentionally create event lazily. / / structure synthesis of events, Const Event = new SyntheticEventCtor(reactName, reactEventType, NULL, nativeEvent, nativeEventTarget,); dispatchQueue.push({event, listeners}); } } else { // Some events don't bubble in the browser. // In the past, React has always bubbled them, but this can be surprising. // We're going to try aligning closer to the browser behavior by not bubbling // them in React either. We'll start by not bubbling onScroll, and then expand. const accumulateTargetOnly = ! inCapturePhase && // TODO: ideally, we'd eventually add all events from // nonDelegatedEvents list in DOMPluginEventSystem. // Then we can remove this special list. // This is a breaking change that can wait until React 18. domEventName === 'scroll'; / / bubbling phase / / collect all nodes to monitor the event listener const listeners = accumulateSinglePhaseListeners (targetInst reactName, nativeEvent.type, inCapturePhase, accumulateTargetOnly, nativeEvent, ); If (listeners. Length > 0) {/ / Intentionally create event lazily. / / structure synthesis of events, Const Event = new SyntheticEventCtor(reactName, reactEventType, NULL, nativeEvent, nativeEventTarget,); dispatchQueue.push({event, listeners}); }}}Copy the code
In SimpleEventPlugin. ExtractEvents function:
-
We first assign SyntheticEvent to SyntheticEventCtor. SyntheticEvent is an object (constructor) inside React that acts as a cross-browser wrapper for native events. Have the same interface as browser native event (stopPropagation, preventDefault) to prevent event compatibility between different browsers.
-
The corresponding synthetic event constructor, SyntheticEventCtor, is then initialized based on the native event name.
-
All listeners on the node listening for the event are then collected during the capture phase and the bubble phase, respectively.
-
Finally, synthetic events are constructed using the synthetic event constructor SyntheticEventCtor and added to the dispatch event queue.
We want to see the next function is accumulateSinglePhaseListeners, the function of this function is to collect all nodes to monitor the event listener.
accumulateSinglePhaseListeners
// packages/react-dom/src/events/DOMPluginEventSystem.js export function accumulateSinglePhaseListeners( targetFiber: Fiber | null, reactName: string | null, nativeEventType: string, inCapturePhase: boolean, accumulateTargetOnly: boolean, nativeEvent: AnyNativeEvent, ): Array<DispatchListener> { const captureName = reactName ! == null ? reactName + 'Capture' : null; const reactEventName = inCapturePhase ? captureName : reactName; let listeners: Array<DispatchListener> = []; let instance = targetFiber; let lastHostComponent = null; // Accumulate all instances and listeners via the target. Until the root position while (instance! == null) { const {stateNode, tag} = instance; Listeners that are on HostComponents (i.e. <div>) // Handle listeners if (tag === HostComponent && stateNode) // Process listeners on native tags ! == null) { lastHostComponent = stateNode; // createEventHandle listeners if (enableCreateEventHandleAPI) { const eventHandlerListeners = getEventHandlerListeners( lastHostComponent, ); if (eventHandlerListeners ! == null) { eventHandlerListeners.forEach(entry => { if ( entry.type === nativeEventType && entry.capture === inCapturePhase ) { listeners.push( createDispatchListener( instance, entry.callback, (lastHostComponent: any), ), ); }}); Notice} // Notice on* listeners, i.e. onClick or onClickCapture If (reactEventName! == null) { const listener = getListener(instance, reactEventName); if (listener ! = null) { listeners.push( createDispatchListener(instance, listener, lastHostComponent), ); } } } else if ( enableCreateEventHandleAPI && enableScopeAPI && tag === ScopeComponent && lastHostComponent ! == null && stateNode ! == null ) { // Scopes const reactScopeInstance = stateNode; const eventHandlerListeners = getEventHandlerListeners( reactScopeInstance, ); if (eventHandlerListeners ! == null) { eventHandlerListeners.forEach(entry => { if ( entry.type === nativeEventType && entry.capture === inCapturePhase ) { listeners.push( createDispatchListener( instance, entry.callback, (lastHostComponent: any), ), ); }}); } } // If we are only accumulating events for the target, Then we don't propagate through the React fiber tree to find other // listeners. If (accumulateTargetOnly) {break; } // If we are processing the onBeforeBlur event, then we need to take // into consideration that part of the React tree might have been hidden // or deleted (as we're invoking this event during commit). We can find // this out by checking if intercept fiber set on the event matches the // current instance fiber. In which case, we should clear all existing // listeners. if (enableCreateEventHandleAPI && nativeEvent.type === 'beforeblur') { // $FlowFixMe: internal field const detachedInterceptFiber = nativeEvent._detachedInterceptFiber; if ( detachedInterceptFiber ! == null && (detachedInterceptFiber === instance || detachedInterceptFiber === instance.alternate) ) { listeners = []; }} // take the parent node and iterate over instance = instance.return; } return listeners; }Copy the code
AccumulateSinglePhaseListeners function code while looking at a very long, but it only did one thing: Start at the fiber node where the event was triggered and work your way up to the root DOM container (div#root), collecting all the listeners and returning them.
Let’s take a look at how React constructs native events into composite events.
SyntheticEventCtor — Constructs composite events
SyntheticEventCtor is a constructor that can be implemented differently depending on the event. In SimpleEventPlugin. ExtractEvents function, according to different event, given the corresponding event handlers.
switch (domEventName) { case 'keypress': // 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: any): KeyboardEvent)) === 0) { return; } /* falls through */ case 'keydown': case 'keyup': SyntheticEventCtor = SyntheticKeyboardEvent; // keyboard compositing event break; case 'focusin': reactEventType = 'focus'; SyntheticEventCtor = SyntheticFocusEvent; // focus synthesis event break; case 'focusout': reactEventType = 'blur'; SyntheticEventCtor = SyntheticFocusEvent; // focus synthesis event break; case 'beforeblur': case 'afterblur': SyntheticEventCtor = SyntheticFocusEvent; // focus synthesis event break; case 'click': // Firefox creates a click event on right mouse clicks. This removes the // unwanted click events. if (nativeEvent.button === 2) { return; } /* falls through */ case 'auxclick': case 'dblclick': case 'mousedown': case 'mousemove': case 'mouseup': // TODO: Disabled elements should not respond to mouse events /* falls through */ case 'mouseout': case 'mouseover': case 'contextmenu': SyntheticEventCtor = SyntheticMouseEvent; // mouse synthesizes the event break; case 'drag': case 'dragend': case 'dragenter': case 'dragexit': case 'dragleave': case 'dragover': case 'dragstart': case 'drop': SyntheticEventCtor = SyntheticDragEvent; // Drag the synthesized event break; case 'touchcancel': case 'touchend': case 'touchmove': case 'touchstart': SyntheticEventCtor = SyntheticTouchEvent; // Move the touch synthesis event break; case ANIMATION_END: case ANIMATION_ITERATION: case ANIMATION_START: SyntheticEventCtor = SyntheticAnimationEvent; // Animate the compositing event break; case TRANSITION_END: SyntheticEventCtor = SyntheticTransitionEvent; // Animate the compositing event break; case 'scroll': SyntheticEventCtor = SyntheticUIEvent; // roll the synthesis event break; case 'wheel': SyntheticEventCtor = SyntheticWheelEvent; // add event break; case 'copy': case 'cut': case 'paste': SyntheticEventCtor = SyntheticClipboardEvent; // Copy/paste/cut the synthesized event break; case 'gotpointercapture': case 'lostpointercapture': case 'pointercancel': case 'pointerdown': case 'pointermove': case 'pointerout': case 'pointerover': case 'pointerup': SyntheticEventCtor = SyntheticPointerEvent; break; default: // Unknown event. This is used by createEventHandle. break; }Copy the code
In the packages/react – dom/SRC/events/SyntheticEvent js file, we can see that these different event handler is called createSyntheticEvent function creates a new function, It’s just that the EventInterfaceType passed in is different.
createSyntheticEvent
// packages/react-dom/src/events/SyntheticEvent.js // This is intentionally a factory so that we have different returned constructors. // If we had a single constructor, it would be megamorphic and engines would deopt. function createSyntheticEvent(Interface: EventInterfaceType) { /** * Synthetic events are dispatched by event plugins, typically in response to a * top-level event delegation handler. * * These systems should generally use pooling to reduce the frequency of garbage * collection. The system should check `isPersistent` to determine whether the * event should be released into the pool after being dispatched. Users that * need a persisted event should invoke `persist`. * * Synthetic events (and subclasses) implement the DOM Level 3 Events API by * normalizing browser quirks. Subclasses do not necessarily have to implement a * DOM interface; Custom application-specific events can also subclass this. */ / string | null, reactEventType: string, targetInst: Fiber, nativeEvent: {[propName: string]: mixed}, nativeEventTarget: null | EventTarget, ) { this._reactName = reactName; this._targetInst = targetInst; this.type = reactEventType; this.nativeEvent = nativeEvent; this.target = nativeEventTarget; this.currentTarget = null; for (const propName in Interface) { if (! Interface.hasOwnProperty(propName)) { continue; } const normalize = Interface[propName]; if (normalize) { this[propName] = normalize(nativeEvent); } else { this[propName] = nativeEvent[propName]; } } const defaultPrevented = nativeEvent.defaultPrevented ! = null ? nativeEvent.defaultPrevented : nativeEvent.returnValue === false; if (defaultPrevented) { this.isDefaultPrevented = functionThatReturnsTrue; } else { this.isDefaultPrevented = functionThatReturnsFalse; } this.isPropagationStopped = functionThatReturnsFalse; return this; } Object. The assign (SyntheticBaseEvent prototype, {/ / in synthesis on the prototype of the constructor add native events of the preventDefault method, is used to prevent events the default behavior of the preventDefault: function() { this.defaultPrevented = true; const event = this.nativeEvent; if (! event) { return; } // Browser compatibility to prevent event default behavior if (event.preventDefault) {event.preventdefault (); // $FlowFixMe - flow is not aware of `unknown` in IE } else if (typeof event.returnValue ! == 'unknown') { event.returnValue = false; } this.isDefaultPrevented = functionThatReturnsTrue; }, // Add the native event stopPropagation method to the prototype of the synthesized event constructor to prevent events from bubblingto the parent element stopPropagation: function() { const event = this.nativeEvent; if (! event) { return; If (event.stopPropagation) {event.stopPropagation(); // $FlowFixMe - flow is not aware of `unknown` in IE } else if (typeof event.cancelBubble ! == 'unknown') { // The ChangeEventPlugin registers a "propertychange" event for // IE. This event does not support bubbling or cancelling, and // any references to cancelBubble throw "Member not found". A // typeof check of "unknown" circumvents this issue (and is also // IE specific). event.cancelBubble = true; } this.isPropagationStopped = functionThatReturnsTrue; }, /** * We release all dispatched `SyntheticEvent`s after each event loop, adding * them back into the pool. This allows a way to hold onto a reference that * won't be added back into the pool. */ persist: function() { // Modern event system doesn't use pooling. }, /** * Checks if this event should be released back into the pool. * * @return {boolean} True if this should not be released, false otherwise. */ isPersistent: functionThatReturnsTrue, }); return SyntheticBaseEvent; }Copy the code
CreateSyntheticEvent is a factory function from which handlers for different synthetic events are constructed.
In the createSyntheticEvent function, a synthetic event constructor, SyntheticBaseEvent, is defined. In addition to basic instance attributes such as _reactName, _targetInst, Type, nativeEvent, Target, and currentTarget, attributes of native events are added to instance objects when constructing corresponding synthetic event handlers. The relevant codes are as follows:
This._reactname = reactName; this._targetInst = targetInst; this.type = reactEventType; this.nativeEvent = nativeEvent; this.target = nativeEventTarget; this.currentTarget = null; For (const propName in Interface) {if (! Interface.hasOwnProperty(propName)) { continue; } const normalize = Interface[propName]; if (normalize) { this[propName] = normalize(nativeEvent); } else { this[propName] = nativeEvent[propName]; }}Copy the code
Add preventDefault and stopPropagation to the Prototype SyntheticBaseEvent using object.asign (). React composite events have the same interface as browser native events. It also smoothes out event compatibility issues between browsers, making the development experience better. The code is as follows:
Object. The assign (SyntheticBaseEvent prototype, {/ / in synthesis on the prototype of the constructor add native events of the preventDefault method, is used to prevent events the default behavior of the preventDefault: function() { this.defaultPrevented = true; const event = this.nativeEvent; if (! event) { return; } // Browser compatibility to prevent event default behavior if (event.preventDefault) {event.preventdefault (); // $FlowFixMe - flow is not aware of `unknown` in IE } else if (typeof event.returnValue ! == 'unknown') { event.returnValue = false; } this.isDefaultPrevented = functionThatReturnsTrue; }, // Add the native event stopPropagation method to the prototype of the synthesized event constructor to prevent events from bubblingto the parent element stopPropagation: function() { const event = this.nativeEvent; if (! event) { return; If (event.stopPropagation) {event.stopPropagation(); // $FlowFixMe - flow is not aware of `unknown` in IE } else if (typeof event.cancelBubble ! == 'unknown') { // The ChangeEventPlugin registers a "propertychange" event for // IE. This event does not support bubbling or cancelling, and // any references to cancelBubble throw "Member not found". A // typeof check of "unknown" circumvents this issue (and is also // IE specific). event.cancelBubble = true; } this.isPropagationStopped = functionThatReturnsTrue; }, /** * We release all dispatched `SyntheticEvent`s after each event loop, adding * them back into the pool. This allows a way to hold onto a reference that * won't be added back into the pool. */ persist: function() { // Modern event system doesn't use pooling. }, /** * Checks if this event should be released back into the pool. * * @return {boolean} True if this should not be released, false otherwise. */ isPersistent: functionThatReturnsTrue, });Copy the code
After the composite event is constructed, it is placed in a dispatchQueue, along with the listener collected, for dispatch. Let’s take a look at React event dispatches.
ProcessDispatchQueue — Performs dispatch
// packages/react-dom/src/events/DOMPluginEventSystem.js export function processDispatchQueue( dispatchQueue: DispatchQueue, eventSystemFlags: EventSystemFlags, ): void { const inCapturePhase = (eventSystemFlags & IS_CAPTURE_PHASE) ! = = 0; for (let i = 0; i < dispatchQueue.length; i++) { const {event, listeners} = dispatchQueue[i]; / / to distribute processDispatchQueueItemsInOrder synthetic events (event listeners, inCapturePhase); // event system doesn't use pooling. } // This would be a good time to rethrow if any of the event handlers threw. rethrowCaughtError(); }Copy the code
ProcessDispatchQueue implementation logic is very simple, traverse dispatchQueue, call processDispatchQueueItemsInOrder function, distributing on synthetic events.
processDispatchQueueItemsInOrder
// packages/react-dom/src/events/DOMPluginEventSystem.js function processDispatchQueueItemsInOrder( event: ReactSyntheticEvent, dispatchListeners: Array<DispatchListener>, inCapturePhase: boolean, ): void { let previousInstance; Listener for (let I = dispatchListeners. Length - 1; i >= 0; i--) { const {instance, currentTarget, listener} = dispatchListeners[i]; / / event. IsPropagationStopped () / / stopPropagation () method is called will return true, no call will return false if (the instance! == previousInstance && event.isPropagationStopped()) { return; } executeDispatch(event, listener, currentTarget); previousInstance = instance; }} else {// listener for (let I = 0; i < dispatchListeners.length; i++) { const {instance, currentTarget, listener} = dispatchListeners[i]; if (instance ! == previousInstance && event.isPropagationStopped()) { return; } executeDispatch(event, listener, currentTarget); previousInstance = instance; }}}Copy the code
In processDispatchQueueItemsInOrder function through all of the current event listener, distributed execution executeDispatch events, binding on fiber node listener is executed.
In the process of sending events, different traversal methods of dispatchListeners are taken according to different capture and bubbling stages:
- Capture phase: Call the callback function bound in the Fiber tree from top to bottom, so traverse the dispatchListeners from back to front
- Bubbling phase: Call the callback function bound in the Fiber tree from bottom up, so traverse the dispatchListeners from front to back
The flow chart
The React composite event is triggered at this point. Let’s review its process through a stream:
Let’s summarize this briefly:
-
Before the event is triggered, the corresponding Fiber node is first found according to the DOM element that actually triggered the event, and the native DOM is associated with the Fiber tree.
-
Then use the React event plugin system to collect all event listeners.
-
The synthetic event is then constructed using the SyntheticBaseEvent constructor to smooth out event compatibility issues across browsers.
-
Finally, the dispatchListeners are traversed in different ways during the capture phase and the bubble phase respectively, and the executeDispatch event is dispatched. The Listener bound to the Fiber node is executed.
conclusion
In terms of experience, React composite events smooth out browser differences, enabling React developers to focus on business logic instead of browser compatibility. On the other hand, events can be managed in a unified manner, improving performance.
Architecturally, React composite events open the interaction channel from external native events to the internal Fiber tree. React can sense native events provided by the browser and React differently, such as modifying the Fiber tree or changing views.
In terms of implementation, it is mainly divided into four steps:
-
Listen for native events, associate native DOM elements with fiber trees, align native DOM nodes with fiber nodes
-
Traverse the Fiber tree and collect all listeners for the event through the event plug-in system
-
The synthetic event of this event is constructed using the constructor SyntheticBaseEvent
-
The dispatchListeners run the executeDispatch process last time