This article is based on React 17.0.0 and analyzes the React-DOM in Legacy Mode
The introduction
React’s composite event system has always been one of React’s signature features. The effect is to add an intermediate layer between developers and real events, allowing developers to output apis that match their design intentions, and native events to be hijacked and manipulated. The virtual DOM adopts a similar layered design, with a layer of abstraction between the developer and the native DOM. This abstract design method is worth learning in depth, and synthesizing the event system is an indispensable step to understand the React principle, hence this article. If there are any mistakes, please correct them.
consistency
In order to facilitate understanding and prevent disagreement, it is necessary to highlight and explain some concepts that may be confusing in this article. Please note that definitions are only responsible for this article and do not necessarily apply to content outside of this article.
Noun form
Nouns name | paraphrase |
---|---|
Real events | An event written in a component by a developer, attempted to mount on the DOM, or understood as a real event. |
Native events | That exists and happens in the browserUI Events, such as ‘click’, ‘cancel’, etc. |
Composite event object | Encapsulated by ReactThe Event object. |
Synthetic event system | React refers to the entire system that handles the event mechanism. |
Root DOM node | Not the Document node, but the DOM node mounted by the React application. |
The event agent | It is also called event delegation, which is the same as event delegation. I think the term “agency” is more suitable for use in this article, so I hereby state:) |
The purpose of synthesizing event systems
Since there are no official answers, I can only offer some subjective ones: Performance optimization: using event brokers to uniformly receive native event firing, so that events are not bound on the real DOM. (Conversely, too many meaningless event collections can be triggered.) Layered design: Solve cross-platform problems. Compose event objects: Smooth out browser differences. React is used to handle native events first and then React is passed to real events. React can know which native events are triggered, which native events invoke the corresponding real events, and which events are triggered in the real events related to React state changes. One reason why the current React synthetic event system is irreplaceable is that React needs to know what native event triggered the update. React hostage event trigger lets you know what event was triggered by the user and what real event was invoked through the native event. In this way, the priority of the real event can be determined by defining the priority of the native event, which can then determine the priority of the update triggered within the real event, and finally determine when the corresponding update should be updated.
The initial combination is the event system
The initialization part of the work is relatively simple, the core purpose is to generate some static variables for later use. I will briefly cover important static variables and will not go into their initialization because the source code is very convoluted for such trivial matters. Entrance to the program in the react – dom/SRC/events/DOMPluginEventSystem js# L89 – L93, interested can debug. The five EventPlugin relationships can be understood as SimpleEventPlugin being the basic functional implementation of the composite event system, while the other eventplugins are merely polyfills of it.
SimpleEventPlugin.registerEvents();
EnterLeaveEventPlugin.registerEvents();
ChangeEventPlugin.registerEvents();
SelectEventPlugin.registerEvents();
BeforeInputEventPlugin.registerEvents();
Copy the code
eventPriorities
Mapping of native events and their priorities
{
"cancel": 0.// ...
"drag": 1.// ...
"abort": 2.// ...
}
Copy the code
React defines priorities for native events in three main categories
export const DiscreteEvent: EventPriority = 0; // Discrete events, such as cancel, click, and mousedown, are of the lowest priority
export const UserBlockingEvent: EventPriority = 1; // User blocking events, such as Drag, mousemove, wheel, etc
export const ContinuousEvent: EventPriority = 2; // Continuous events, load, error, waiting and other media-related events require timely response, so they have the highest priority
Copy the code
topLevelEventsToReactNames
Mapping of native and synthesized events
{
"cancel": "onCancel".// ...
"pointercancel": "onPointerCancel".// ...
"waiting": "onWaiting"
}
Copy the code
registrationNameDependencies
A mapping of synthesized events to the set of native events on which they depend
{
"onCancel": ["cancel"]."onCancelCapture": ["cancel"].// ...
"onChange": ["change"."click"."focusin"."focusout"."input"."keydown"."keyup"."selectionchange"]."onCancelCapture": ["change"."click"."focusin"."focusout"."input"."keydown"."keyup"."selectionchange"]."onSelect": ["focusout"."contextmenu"."dragend"."focusin"."keydown"."keyup"."mousedown"."mouseup"."selectionchange"]."onSelectCapture": ["focusout"."contextmenu"."dragend"."focusin"."keydown"."keyup"."mousedown"."mouseup"."selectionchange"].// ...
}
Copy the code
other
In addition, some variables or constants that are not all dynamically generated are also briefly introduced:
variable | The data type | meaning |
---|---|---|
allNativeEvents | Set | A collection of all meaningful native event names |
nonDelegatedEvents | Set | A collection of native event names that do not need to be brokered (delegated) during the bubbling phase |
Register event broker
17. X has changed greatly compared with 16. X, please pay attention to screening.
React uses the event broker to capture native events that occur in the browser, and then uses the native eventsEvent
Object to collect real events and then call real events.
We’ll talk about collecting events later, but let’s focus on the first half.”The event agent“At what stage and how it was done.
In version 17.x, createReactRoot
Phase is calledlistenToAllSupportedEvents
Delta function and delta functionAll native events that can be listened onAdd listening events to.
The call chain is as follows:
listenToAllSupportedEvents
export function listenToAllSupportedEvents(rootContainerElement: EventTarget) {
// The rootContainerElement element is passed in by the function that creates ReactRoot. Its content is the root DOM node of the React application.
// enableEagerRootListeners are constant identifiers, often true, and can be ignored.
// This means that the "add listeners on all native events as early as possible" feature is enabled, as opposed to the 16.x version where listeners are added later on demand.
if (enableEagerRootListeners) {
// listeningMarker is a marker consisting of fixed and random characters. It is used to identify whether the node has added listening events on all native events in react mode.
// If it has already been added, skip it to save some unnecessary work
if ((rootContainerElement: any)[listeningMarker]) {
return;
}
// Add the identity
(rootContainerElement: any)[listeningMarker] = true;
// Iterate over all native events
// In addition to native events that do not need to add event agents during the bubble phase, only the capture phase should add event agents
// The rest of the events need to be added in the capture, bubble phase
allNativeEvents.forEach(domEventName= > {
if(! nonDelegatedEvents.has(domEventName)) { listenToNativeEvent( domEventName,false,
((rootContainerElement: any): Element),
null,); } listenToNativeEvent( domEventName,true,
((rootContainerElement: any): Element),
null,); }); }}Copy the code
listenToNativeEvent
// Simplified version
export function listenToNativeEvent(
domEventName: DOMEventName,
isCapturePhaseListener: boolean,
rootContainerElement: EventTarget,
targetElement: Element | null, eventSystemFlags? : EventSystemFlags =0.) :void {
let target = rootContainerElement;
// ...
// The target node stores a value of type Set, which stores the native event names of listeners added to prevent repeated listener addition.
const listenerSet = getEventListenerSet(target);
/ / effect: 'cancel' - > 'cancel__capture' | 'cancel__bubble'
// Get the name of the event to be placed in the listenerSet
const listenerSetKey = getListenerSetKey(domEventName, isCapturePhaseListener);
// If not, bind
if(! listenerSet.has(listenerSetKey)) {// At this stage, the eventSystemFlags input parameter is always 0.
EventSystemFlags = IS_CAPTURE_PHASE = 1 << 2 as long as the listener is added in the capture phase.
if (isCapturePhaseListener) {
eventSystemFlags |= IS_CAPTURE_PHASE;
}
addTrappedEventListener(
target,
domEventName,
eventSystemFlags,
isCapturePhaseListener,
);
// Add to listenerSetlistenerSet.add(listenerSetKey); }}Copy the code
addTrappedEventListener
// Simplified version
function addTrappedEventListener(
targetContainer: EventTarget,
domEventName: DOMEventName,
eventSystemFlags: EventSystemFlags,
isCapturePhaseListener: boolean, isDeferredListenerForLegacyFBSupport? :boolean.) {
// Create event listeners with priority, as outlined below
let listener = createEventListenerWrapperWithPriority(
targetContainer,
domEventName,
eventSystemFlags,
);
// ...
let unsubscribeListener;
// Add event listeners of different stages to native events
if (isCapturePhaseListener) {
// ...
unsubscribeListener = addEventCaptureListener(
targetContainer,
domEventName,
listener,
);
} else {
// ...unsubscribeListener = addEventBubbleListener( targetContainer, domEventName, listener, ); }}Copy the code
CreateEventListenerWrapperWithPriority (create the listener with a priority)
export function createEventListenerWrapperWithPriority(targetContainer: EventTarget, domEventName: DOMEventName, eventSystemFlags: EventSystemFlags,) :Function {
// Get the priority of the current native event from eventPriorities mentioned above
const eventPriority = getEventPriorityForPluginSystem(domEventName);
let listenerWrapper;
// Different listener functions are provided according to different priorities
switch (eventPriority) {
case DiscreteEvent:
listenerWrapper = dispatchDiscreteEvent;
break;
case UserBlockingEvent:
listenerWrapper = dispatchUserBlockingUpdate;
break;
case ContinuousEvent:
default:
listenerWrapper = dispatchEvent;
break;
}
// All listeners have the same input parameters:
// (domEventName: DOMEventName, eventSystemFlags: EventSystemFlags, targetContainer: EventTarget, nativeEvent: AnyNativeEvent) => void
// The first three arguments are provided by the current function, and the last argument is the Event object, the only input argument that native listeners will have
return listenerWrapper.bind(
null,
domEventName,
eventSystemFlags,
targetContainer,
);
}
Copy the code
AddEventCaptureListener/addEventBubbleListener (mount listener)
No further details
export function addEventBubbleListener(
target: EventTarget,
eventType: string,
listener: Function.) :Function {
target.addEventListener(eventType, listener, false);
return listener;
}
export function addEventCaptureListener(
target: EventTarget,
eventType: string,
listener: Function.) :Function {
target.addEventListener(eventType, listener, true);
return listener;
}
Copy the code
summary
Finally, on the root DOM node, each native event is bound to its corresponding priorityThe listener
Trigger event beginning
Up to this point, everything in this article can be understood as preparation for the composite event system until the page is rendered into a composite event system. After normal rendering is completed, when the browser invokes the native event, the synthetic event system will start to work 👷♂️. The listener mentioned above will receive the event of the browser, and then pass down the information to collect relevant events to simulate the triggering process of the browser and achieve the expected triggering effect. So how does the React composite event system work?
Results the overview
I want to give you a general idea before I explain it, just to give you an impression, and then you can look at it when you explain it.
The test Demo is as follows:
Three events were writtenonClick
,onDrag
,onPlaying
, respectively corresponding to the events of three priorities in the composite event system, respectively triggering corresponding listeners.
The call stack after each event invocation is as follows from top to bottom:
We’ll focus on the first half of each call stack to understand how the React composite event system works.
The React update in the second half is beyond the scope of this article, so we won’t go into details.
Listener entry
We know from the call stack and above that when we click the button on the page. For the onClick example, the first thing to fire is the listener for the Click event that is mounted on the root DOM node, i.e. DispatchDiscreteEvent. Review, in this time “registered agent – createEventListenerWrapperWithPriority” as mentioned in section, will React according to the different priorities for different listener, the listener three kinds, respectively is:
- DiscreteEvent listener: dispatchDiscreteEvent
- User blocking event listener: dispatchUserBlockingUpdate
- Continuous event or other event listener: dispatchEvent
First, the purpose of these three listeners is the same, the ultimate purpose is to collect events, event invocation. At the code level, as you can see from the call stack, the dispatchEvent (third type listener) function is called. The difference, however, is that what happens to listeners before they call dispatchEvent is different. Continuous events or other event listeners (the third type of listener) are called directly synchronously because they have the highest priority, whereas the other two types are different. So what we need to understand now is what the first two types of listeners do before calling dispatchEvent and why, and then focus on what dispatchEvent does. Due to the complexity of their content, the first two listeners, the author will first talk about the second type of listener “user blocking event listener” for readers to understand.
DispatchUserBlockingUpdate (user blocking event listener)
The content of the function is simple: a function runWithPriority is called, passing in the priority of the current task and the task you want to execute (the function). RunWithPriority marks the priority of the current task in a global static variable so that internal updates know which priority event is currently being executed. It is also possible to explain why the third type of listener calls dispatchEvent directly without any side effects because it is high priority and can be called synchronously.
// Simplified version
// The first three parameters are passed in when the event broker is registered,
// domEventName: indicates the name of the original event
// eventSystemFlags can only be 4 or 0, representing capture phase events and bubble phase events respectively
// container: application root DOM node
// nativeEvent: The Event object passed by the native listener
function dispatchUserBlockingUpdate(domEventName, eventSystemFlags, container, nativeEvent) {
runWithPriority(
UserBlockingPriority,
dispatchEvent.bind(
null,
domEventName,
eventSystemFlags,
container,
nativeEvent,
),
)
}
Copy the code
DispatchDiscreteEvent (DiscreteEvent Listener)
The first type listener is a bit more complicated than the second type listener, and the following is a partial call chain from top to bottom
function dispatchDiscreteEvent(domEventName, eventSystemFlags, container, nativeEvent) {
/ / flushDiscreteUpdatesIfNeeded role is to clear the previously saved to perform the discrete tasks, including but not limited to, the discrete event before the trigger and useEffect callback,
// To ensure that the state of the current discrete event is up to date
// If you have a headache, you can pretend it doesn't exist
flushDiscreteUpdatesIfNeeded(nativeEvent.timeStamp);
// Create a new discrete update
// The last four arguments are actually arguments to the first function argument
// Call dispatchEvent(domEventName, eventSystemFlags, Container, nativeEvent)
discreteUpdates(
dispatchEvent,
domEventName,
eventSystemFlags,
container,
nativeEvent,
);
}
export function discreteUpdates(fn, a, b, c, d) {
// Marks that the event is currently being processed and stores the previous state
const prevIsInsideEventHandler = isInsideEventHandler;
isInsideEventHandler = true;
try {
// The current function only needs to focus on this line
// Call the discrete update function in Scheduler
return discreteUpdatesImpl(fn, a, b, c, d);
} finally {
// Continue if you were already in the event process
isInsideEventHandler = prevIsInsideEventHandler;
if(! isInsideEventHandler) { finishEventHandler(); }}}// A discrete update function in the react-Reconciler
// It does two things, the first is to call the corresponding discrete event, and the second is to update the updates that might be generated in the discrete event (if the timing is right)
// If you look at the discrete event listener call stack above, you will see that the two things here are respectively
// First thing: the synthetic event system collects real events and calls real events
// Second thing: Update any updates that might be generated in a real event
discreteUpdatesImpl = function discreteUpdates<A.B.C.D.R> (fn: (A, B, C) => R, a: A, b: B, c: C, d: D,) :R {
// Add the current execution context state, which is used to determine the current situation, such as RenderContext, which indicates that the update is in the render phase
// All context types https://github.com/facebook/react/blob/v17.0.0/packages/react-reconciler/src/ReactFiberWorkLoop.old.js#L249-L256
const prevExecutionContext = executionContext;
executionContext |= DiscreteEventContext;
try {
// This is the same as the second type of listener.
return runWithPriority(
UserBlockingSchedulerPriority,
fn.bind(null, a, b, c, d),
);
} finally {
Return to previous context
executionContext = prevExecutionContext;
// If there is no execution context, the execution is complete, and you can start updating the generated updates
if (executionContext === NoContext) {
// Flush the immediate callbacks that were scheduled during this batchresetRenderTimer(); flushSyncCallbackQueue(); }}}Copy the code
After the dispatchEvent
From the above description, I believe you must be cleardispatchEvent
It is the end of all the fancy listeners that contain the core functionality of the synthetic event system.
The reader can start by reviewing the call stack shown aboveonClick
For example:
In factdispathEvent
At the heart of this is invocationdispatchEventsForPlugins
Because it’s this function that firesEvent collection and event execution.
Before that, he would do a series of tedious scheduling and judgment of edge cases, which are of little reference value for the main process. Therefore, the author intends to skip the explanation of these contents and go straight to the theme, and only explain the source of its input.
dispatchEventsForPlugins
The fourth input parameter, targetInst, has a data type of Fiber or NULL, which is usually just Fiber, so you can ignore the possibility of null to avoid mental overload. In the case of the click event in the previous test Demo, targetInst is the Fiber node corresponding to the we clicked. AttemptToDispatchEvent React obtains the Fiber node in the attemptToDispatchEvent function that appears in the call stack, and there are two steps:
- Gets the object passed in from the listener
Event
Object and getEvent.target
The DOM node in, this DOM node is actually<button />
.Access to functions - Gets the object stored on the DOM node
Fiber
Node,Fiber
The node actually existsDOM.['__reactFiber$' + randomKey]
The key value of.Access to functions.The corresponding assignment function
function dispatchEventsForPlugins(
domEventName: DOMEventName, // Event name
eventSystemFlags: EventSystemFlags, // Event processing phase, 4 = capture phase, 0 = bubble phase
nativeEvent: AnyNativeEvent, // The listener's native input Event object
targetInst: null | Fiber, // Event. target corresponds to the Fiber node of the DOM node
targetContainer: EventTarget, // Root DOM node
) :void {
// Get an event.target
const nativeEventTarget = getEventTarget(nativeEvent);
// Event queue, where collected events are stored
const dispatchQueue: DispatchQueue = [];
// Collect events
extractEvents(
dispatchQueue,
domEventName,
targetInst,
nativeEvent,
nativeEventTarget,
eventSystemFlags,
targetContainer,
);
// Execute the event
processDispatchQueue(dispatchQueue, eventSystemFlags);
}
Copy the code
ExtractEvents (Collect events)
The content of the extractEvents is actually very simple, the extractEvents of several EventPlugin are called on demand, the purpose of which is the same, but different events may be generated for different events. We will with the most core is the most critical SimpleEventPlugin. ExtractEvents to explain
function extractEvents(
dispatchQueue: DispatchQueue,
domEventName: DOMEventName,
targetInst: null | Fiber,
nativeEvent: AnyNativeEvent,
nativeEventTarget: null | EventTarget,
eventSystemFlags: EventSystemFlags,
targetContainer: EventTarget,
) :void {
// Get the synthesized event name based on the native event name
/ / effect: onClick = topLevelEventsToReactNames. Get (' click ')
const reactName = topLevelEventsToReactNames.get(domEventName);
if (reactName === undefined) {
return;
}
// The constructor of the default synthesis function
let SyntheticEventCtor = SyntheticEvent;
let reactEventType: string = domEventName;
switch (domEventName) {
// Get the corresponding synthesized event constructor according to the native event name
}
// Is the capture phase
constinCapturePhase = (eventSystemFlags & IS_CAPTURE_PHASE) ! = =0;
if (
enableCreateEventHandleAPI &&
eventSystemFlags & IS_EVENT_HANDLE_NON_MANAGED_NODE
) {
/ /... Basically the same as below
} else {
// Scroll event does not bubble
constaccumulateTargetOnly = ! inCapturePhase && domEventName ==='scroll';
// core, get all the events of the current phase
const listeners = accumulateSinglePhaseListeners(
targetInst,
reactName,
nativeEvent.type,
inCapturePhase,
accumulateTargetOnly,
);
if (listeners.length > 0) {
// Generate an Event object for the synthesized Event
const event = new SyntheticEventCtor(
reactName,
reactEventType,
null,
nativeEvent,
nativeEventTarget,
);
/ / teamdispatchQueue.push({event, listeners}); }}}Copy the code
accumulateSinglePhaseListeners
// Simplified version
export function accumulateSinglePhaseListeners(
targetFiber: Fiber | null,
reactName: string | null,
nativeEventType: string,
inCapturePhase: boolean,
accumulateTargetOnly: boolean.) :Array<DispatchListener> {
// Capture phase synthesizes event names
constcaptureName = reactName ! = =null ? reactName + 'Capture' : null;
// The final synthesized event name
const reactEventName = inCapturePhase ? captureName : reactName;
const listeners: Array<DispatchListener> = [];
let instance = targetFiber;
let lastHostComponent = null;
while(instance ! = =null) {
const {stateNode, tag} = instance;
// If it is a valid node, get its event
if(tag === HostComponent && stateNode ! = =null) {
lastHostComponent = stateNode;
if(reactEventName ! = =null) {
// Get the corresponding events stored in the Props on the Fiber node (if any)
const listener = getListener(instance, reactEventName);
if(listener ! =null) {
/ / team
listeners.push(
Simply return a {instance, listener, lastHostComponent} objectcreateDispatchListener(instance, listener, lastHostComponent), ); }}}// Scroll will not bubble
if (accumulateTargetOnly) {
break;
}
// Parent Fiber node, recursive up
instance = instance.return;
}
// Returns a collection of listeners
return listeners;
}
Copy the code
ProcessDispatchQueue (Execution event)
Before we look at the source code, let’s take a look at the data type of dispatchQueue, which is generally 1 in length.
interface dispatchQueue {
event: SyntheticEvent
listeners: {
instance: Fiber,
listener: Function.currentTarget: Fiber['stateNode']
}[]
}[]
Copy the code
On the source code
export function processDispatchQueue(dispatchQueue: DispatchQueue, eventSystemFlags: EventSystemFlags,) :void {
// Determine the current event phase using eventSystemFlags
constinCapturePhase = (eventSystemFlags & IS_CAPTURE_PHASE) ! = =0;
// Iterate over the composite event
for (let i = 0; i < dispatchQueue.length; i++) {
// Retrieve the synthesized Event object and the Event collection
const {event, listeners} = dispatchQueue[i];
// This function is responsible for calling events
// If the capture phase Event is called in reverse order, otherwise it is called in positive order, passing in a synthesized Event object
processDispatchQueueItemsInOrder(event, listeners, inCapturePhase);
}
// Throw an intermediate error
rethrowCaughtError();
}
Copy the code
conclusion
The React composite event system is not particularly complex (compared with Concurrent mode code). The core idea is to use event agents to receive events and then schedule the actual event calls by React itself. At the heart of how React knows where to start collecting events are Fiber nodes stored in the real DOM, shuttled from the real to the virtual.
other
Differences from the 16.x version
React V17.0 RC release statement
Mention three strong changes in personal perception
1. The node mounted by the listener is changed from Document node to RootNode (root DOM node)
Related PR: Modern Event System: Add plugin handling and forked Paths #18195
Its main function is to bind and synthesize the influence range of the event system. Suppose I have a React application in a Web application and other applications also use the Document event monitoring, at this time, there is a high probability of mutual influence leading to errors of one party. This side effect is reduced when we bundle the composite event system into the root DOM node of the current React application.
2. Add all known event listeners when root is mounted, instead of adding listeners on demand in the completeWork stage
PR: Attaching Listeners to Roots and Portal Containers #19659
The main effect is to fix the createPortal event bubble problem, although the problem is not big, but the corresponding source code changes are really many.
3. Remove the event pool
Related PR:
- Remove event pooling in the modern system #18216
- Remove event pooling in the modern system #18969
It was originally designed to improve performance, but there was some mental overhead that led to asynchronous updates in events, so it was removed at this point without much performance impact.
Does Vue have its own event mechanism? How is React different?
Vue actually does a lot of its own content in the event, mainly for the convenience of developers to develop, such as various modifiers or instructions, its core operation mode is still dependent on its template compilation, is a more compile-time feature. Binding events are also mounted on the corresponding DOM one by one in the patch stage, rather than uniformly distributed by event proxy.
reference
- UI Events | W3C Working Draft, 04 August 2016
- The Event – | MDN Web API interface reference
- React V17.0 RC release statement
- Modern Event System: add plugin handling and forked paths #18195
- Attach Listeners Eagerly to Roots and Portal Containers #19659
- Remove event pooling in the modern system #18216
- Remove event pooling in the modern system #18969
- Learn more about the React synthetic event mechanism
- React events and the future