In the browser world, we usually use addEventListener to bind events. For an event to be triggered, there are three phases, in chronological order:
- Capture Phase
- Target Phase
- Bubbing Phase
Accordingly, addEventListener has an input parameter that specifies at which stage the callback will be executed:
target.addEventListener(type, listener, true); // Listen for the capture phase
target.addEventListener(type, listener); // Listen for the bubble phase
Copy the code
The React event system also implements this function. For example, the default onClick event is to listen for the bubble phase. We can use onClickCapture to listen for the capture phase:
<button
className="btn"
onClick={handleClick}
onClickCaputre={handleClickCapture}
>
Hello
</button>
Copy the code
The trigger timing of the two phases is different, resulting in different trigger sequences. React processes the two phases separately.
Next, we’ll look at the source code, but before we do, let’s take a look at the compiled JSX code above:
React.createElement("button", {
className: "btn".onClick: handleClick,
onClickCaputre: handleClickCapture
}, "Hello");
Copy the code
As you can see, onClick is nothing special. Just like className, it is passed as a value for props, and later, just like className, its value is stored on the corresponding Fiber node of this element. So how does it get executed when you click on it? Don’t worry, you’ll understand after reading this article.
The React event system has a lot of nested functions. To avoid confusion, take a look at the process:
// 0. Delegate events on the root nodeapp? .addEventListener('click'.(e) = > {
// 1. Find the e.target attribute to find the currently clicked DOM element
// 2. Find the corresponding Fiber node based on the current DOM element
Collect the onClick function on the link from the Fiber node to the root node
// 4. Distribute the collected functions
})
Copy the code
Now we officially enter the source code interpretation part.
When we call the React. Render or React. CreateRoot initializes the application, we will first create the application’s root level Fiber node, then is called listenToAllSupportedEvents to bind an event:
// rootContainerElement is the DOM element corresponding to the root node of our application
listenToAllSupportedEvents(rootContainerElement);
Copy the code
ListenToAllSupportedEvents traverses allNativeEvents this Set object. AllNativeEvents contains the names of native events such as ‘click’, ‘input’, and ‘focus’. The React source will be injected as soon as it is loaded:
// One of the things these methods do is inject some event names into allNativeEvents
SimpleEventPlugin.registerEvents();
EnterLeaveEventPlugin.registerEvents();
ChangeEventPlugin.registerEvents();
SelectEventPlugin.registerEvents();
BeforeInputEventPlugin.registerEvents();
Copy the code
What see listenToAllSupportedEvents do next.
export function listenToAllSupportedEvents(
rootContainerElement: EventTarget
) {
// Ensure that the event is registered only once and will not be registered the next time this function is called
if(! (rootContainerElement: any)[listeningMarker]) { (rootContainerElement: any)[listeningMarker] =true;
// This is a set structure where values have the names of various native events
allNativeEvents.forEach(domEventName= > {
if(domEventName ! = ='selectionchange') {
// Some special events do not need to be delegated to the root node of the application.
// Such as 'cancel', 'load', 'scroll', etc
if(! nonDelegatedEvents.has(domEventName)) {// Bind the event delegate for the bubbling phase of this event on the root node
listenToNativeEvent(domEventName, false, rootContainerElement);
}
Bind the event delegate for the capture phase of this event on the root node
listenToNativeEvent(domEventName, true, rootContainerElement); }}); }}Copy the code
And then we look at what the listenToNativeEvent does.
export function listenToNativeEvent(
// Native event name
domEventName: DOMEventName,
// True is the capture phase, and vice versa is the bubble phase
isCapturePhaseListener: boolean,
// The node to which the event delegate is added is, for now, the root node
target: EventTarget,
) :void {
let eventSystemFlags = 0;
// Whether to mark the capture phase
if (isCapturePhaseListener) {
eventSystemFlags |= IS_CAPTURE_PHASE;
}
addTrappedEventListener(
target,
domEventName,
eventSystemFlags,
isCapturePhaseListener,
);
}
Copy the code
This piece of code is fairly straightforward. It ends with a call to addTrappedEventListener, which binds events to our root node.
function addTrappedEventListener(targetContainer: EventTarget, domEventName: DOMEventName, eventSystemFlags: EventSystemFlags, isCapturePhaseListener: boolean, isDeferredListenerForLegacyFBSupport? : boolean,) {
// Create the event delegate callback function
let listener = createEventListenerWrapperWithPriority(
targetContainer,
domEventName,
eventSystemFlags,
);
let unsubscribeListener;
// Two different functions are called depending on whether the capture phase or the bubble phase
if (isCapturePhaseListener) {
unsubscribeListener = addEventCaptureListener(
targetContainer,
domEventName,
listener,
);
} else{ unsubscribeListener = addEventBubbleListener( targetContainer, domEventName, listener, ); }}Copy the code
The addEventCaptureListener is very similar to the addEventBubbleListener, except that the last parameter passed to the addEventListener is different, which makes me wonder, why not open a parameter and write it as a function?
export function addEventCaptureListener(
target: EventTarget,
eventType: string,
listener: Function.) :Function {
target.addEventListener(eventType, listener, true);
return listener;
}
export function addEventBubbleListener(
target: EventTarget,
eventType: string,
listener: Function.) :Function {
target.addEventListener(eventType, listener, false);
return listener;
}
Copy the code
In order not to affect the main flow, I have omitted the explanation of isPassiveListener judgments in the addTrappedEventListener above, which basically says: If the browser addEventListener supports passive, and the current binding event is named ‘touchStart’, ‘touchMove’, or ‘wheel’, then by default passive is added to the binding event. This can greatly improve performance. If you want to understand the role of passive, please refer to this answer.
If you want to see all of addTrappedEventListenr, click me to view it.
At this point, we’re done. We’ve bound events to the root node.
Next, when an activity is performed on the page and the event is triggered, the callback function is called. Invoke the callback function in the code above, is called the listener, it is generated by createEventListenerWrapperWithPriority. The code for this function is straightforward:
export function createEventListenerWrapperWithPriority(targetContainer: EventTarget, domEventName: DOMEventName, eventSystemFlags: EventSystemFlags,) :Function {
// Get priority by event name, common events such as 'click' and 'input'
// DiscreteEventPriority
const eventPriority = getEventPriority(domEventName);
let listenerWrapper;
switch (eventPriority) {
case DiscreteEventPriority:
listenerWrapper = dispatchDiscreteEvent;
break;
case ContinuousEventPriority:
listenerWrapper = dispatchContinuousEvent;
break;
case DefaultEventPriority:
default:
listenerWrapper = dispatchEvent;
break;
}
// The return value is our callback function listener
// listenerWrapper actually accepts four arguments and we are currently only binding the first three,
// The fourth is passed in by the callback when the event is raised.
// The Event object for the DOM.
return listenerWrapper.bind(
null,
domEventName,
eventSystemFlags,
targetContainer,
);
}
Copy the code
Let’s take dispatchDiscreteEvent as an example for the following flow. In fact, we found that both it and dispatchContinuousEvent end up calling dispatchEvent.
function dispatchDiscreteEvent(domEventName, eventSystemFlags, container, nativeEvent,) {
const previousPriority = getCurrentUpdatePriority();
const prevTransition = ReactCurrentBatchConfig.transition;
ReactCurrentBatchConfig.transition = 0;
try {
setCurrentUpdatePriority(DiscreteEventPriority);
dispatchEvent(domEventName, eventSystemFlags, container, nativeEvent);
} finally{ setCurrentUpdatePriority(previousPriority); ReactCurrentBatchConfig.transition = prevTransition; }}Copy the code
AttemptToDispatchEvent so, to investigate what dispatchEvent does, I also omit the other code in order not to affect the reading of the main process. All that remains is that it calls the attemptToDispatchEvent method.
export function dispatchEvent(domEventName: DOMEventName, eventSystemFlags: EventSystemFlags, targetContainer: EventTarget, nativeEvent: AnyNativeEvent,) :void {
let blockedOn = attemptToDispatchEvent(
domEventName,
eventSystemFlags,
targetContainer,
nativeEvent,
);
}
Copy the code
The attemptToDispatchEvent method attemptToDispatchEvent is an attempt to dispatch an event, returning null on success and SuspenseInstance or Container on failure (I don’t know what this is, However, this does not affect the flow we read later).
export function attemptToDispatchEvent(domEventName: DOMEventName, eventSystemFlags: EventSystemFlags, targetContainer: EventTarget, nativeEvent: AnyNativeEvent,) :null | Container | SuspenseInstance {
// Get the DOM node currently clicked
// NativeEvent.target is normally read
const nativeEventTarget = getEventTarget(nativeEvent);
// Get the Fiber instance corresponding to the DOM node
let targetInst = getClosestInstanceFromNode(nativeEventTarget);
// The obtained Fiber instance may have some problems, not desired, here do compatibility
// If you don't want to see this, you can ignore it.
if(targetInst ! = =null) {
const nearestMounted = getNearestMountedFiber(targetInst);
if (nearestMounted === null) {
// This tree has been unmounted already. Dispatch without a target.
targetInst = null;
} else {
const tag = nearestMounted.tag;
if (tag === 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; }}}// Collect events on the Fiber node and start dispatching
dispatchEventForPluginEventSystem(
domEventName,
eventSystemFlags,
nativeEvent,
targetInst,
targetContainer,
);
// This is where you would normally go and return null
return null;
}
Copy the code
Look at the above function dispatchEventForPluginEventSystem before returning the last call:
export function dispatchEventForPluginEventSystem(
domEventName: DOMEventName,
eventSystemFlags: EventSystemFlags,
nativeEvent: AnyNativeEvent,
targetInst: null | Fiber,
targetContainer: EventTarget,
) :void {
batchedUpdates(() = >
dispatchEventsForPlugins(
domEventName,
eventSystemFlags,
nativeEvent,
ancestorInst,
targetContainer,
),
);
}
Copy the code
Here, we see that dispatchEventsForPlugins are wrapped by batchedUpdates. I will update a separate blog to explain the batch updates. After all, it’s kind of a waste of time to write it here, because I don’t know if anyone saw it. For now, let’s just ignore it.
The logic of dispatchEventsForPlugins is fairly clear:
function dispatchEventsForPlugins(
domEventName: DOMEventName,
eventSystemFlags: EventSystemFlags,
nativeEvent: AnyNativeEvent,
targetInst: null | Fiber,
targetContainer: EventTarget,
) :void {
// Get the currently clicked DOM node
const nativeEventTarget = getEventTarget(nativeEvent);
const dispatchQueue: DispatchQueue = [];
// Collect events on the entire link from the current Fiber node
extractEvents(
dispatchQueue,
domEventName,
targetInst,
nativeEvent,
nativeEventTarget,
eventSystemFlags,
targetContainer,
);
// Triggers the collected events in turn
processDispatchQueue(dispatchQueue, eventSystemFlags);
}
Copy the code
The remaining functions are mainly extractEvents and processDispatchQueue.
Let’s start with extractEvents. It calls the SimpleEventPlugin extractEvents.
function extractEvents(
dispatchQueue: DispatchQueue,
domEventName: DOMEventName,
targetInst: null | Fiber,
nativeEvent: AnyNativeEvent,
nativeEventTarget: null | EventTarget,
eventSystemFlags: EventSystemFlags,
targetContainer: EventTarget,
) {
SimpleEventPlugin.extractEvents(
dispatchQueue,
domEventName,
targetInst,
nativeEvent,
nativeEventTarget,
eventSystemFlags,
targetContainer,
);
}
Copy the code
As you may know, the React event callback argument is not native. Instead, it has a special handler called a SyntheticEvent. For more information, visit the section on the React event website. The process of generating a SyntheticEvent is in the following code.
Next is the SimpleEventPlugin. ExtractEvents code:
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;
let reactEventType: string = domEventName;
// Find the constructor by the event name
switch (domEventName) {
/ /... Other judgments are omitted
case 'click':
// Firefox creates a click event on right mouse clicks. This removes the
// unwanted click events.
if (nativeEvent.button === 2) {
return;
}
default:
break;
}
constinCapturePhase = (eventSystemFlags & IS_CAPTURE_PHASE) ! = =0;
// Start from the current Fiber node, according to the event name
// Collect listen events from props of the current node
// collect its parent node all the way to the top.
const listeners = accumulateSinglePhaseListeners(
targetInst,
reactName,
nativeEvent.type,
inCapturePhase,
accumulateTargetOnly,
nativeEvent,
);
if (listeners.length > 0) {
// Create a basic EventTarget
const event = new SyntheticEventCtor(
reactName,
reactEventType,
null,
nativeEvent,
nativeEventTarget,
);
// Join the dispatch queuedispatchQueue.push({ event, listeners }); }}Copy the code
As long as understanding how to traverse the tree Fiber, accumulateSinglePhaseListeners code is easy to understand, I ignored here, have a classmate want to see Click here to direct.
When the extractEvents are called, our dispatchQueue has collected all the callback functions and needs to call processDispatchQueue to call them in turn. And this is the last function we’re going to look at.
export function processDispatchQueue(
dispatchQueue: DispatchQueue, // The event array we collected earlier
eventSystemFlags: EventSystemFlags, // Contains information about whether it is the capture phase
) :void {
// True is the capture phase, which affects the execution order of subsequent events
constinCapturePhase = (eventSystemFlags & IS_CAPTURE_PHASE) ! = =0;
for (let i = 0; i < dispatchQueue.length; i++) {
const{event, listeners} = dispatchQueue[i]; processDispatchQueueItemsInOrder(event, listeners, inCapturePhase); }}Copy the code
Note that dispatchListeners contain all callbacks that need to be executed from the current element to the root node, where the root node is the last. Since it is, the capture phase and bubble phase sequence would be different, for this reason, look at processDispatchQueueItemsInOrder:
function processDispatchQueueItemsInOrder(
event: ReactSyntheticEvent,
dispatchListeners: Array<DispatchListener>,
inCapturePhase: boolean,
) :void {
let previousInstance;
if (inCapturePhase) {
// The last event is executed first in the capture phase
for (let i = dispatchListeners.length - 1; i >= 0; i--) {
const {instance, currentTarget, listener} = dispatchListeners[i];
// Check whether the bubble has stopped, if so, do not perform the following, directly return
if(instance ! == previousInstance && event.isPropagationStopped()) {return; } executeDispatch(event, listener, currentTarget); previousInstance = instance; }}else {
// The bubble phase is executed first
for (let i = 0; i < dispatchListeners.length; i++) {
const {instance, currentTarget, listener} = dispatchListeners[i];
// Check whether the bubble has stopped, if so, do not perform the following, directly return
if(instance ! == previousInstance && event.isPropagationStopped()) {return; } executeDispatch(event, listener, currentTarget); previousInstance = instance; }}}Copy the code
Why the event. IsPropagationStopped can have the effect of prevent bubble?
The React event object overrides the stopPropagation method. At this point, all events come from a parent class. However, when a function executes e.topPropagation during event processing, the parent isPropagationStopped method is modified, and all subsequent functions return true.
stopPropagation: function() {
const event = this.nativeEvent;
if(! event) {return;
}
if (event.stopPropagation) {
event.stopPropagation();
}
this.isPropagationStopped = functionThatReturnsTrue;
}
/ /... Leave out the rest...
function functionThatReturnsTrue() {
return true;
}
Copy the code
React synthesizes events in a way that is easy to understand if you are careful.
After watching React’s synthetic event mechanism, I felt sorry for my ignorance before. I thought React was not done by event delegation before, and I despised the event module of my company’s framework.
Hope this article helped you understand it, and thank you very much for reading it.