Start with a simple requirement.
Requirements describe
Click the button to bring up a dialog box. Click again to close the dialog box. You can also close the dialog box by clicking on the blank area outside the dialog box.
Code implementation
class Demo extends PureComponent {
state = {
visible: false};componentDidMount() {
document.body.addEventListener('click', () => {
this.setState({
visible: false}); }); }componentWillUnmount() {
document.body.removeEventListener('click'); } handleBtnClick = (e) => { e.preventDefault(); const { visible } = this.state; this.setState({ visible: ! visible, }); } handleDialogClick = (e) => { e.preventDefault(); }render() {
const { visible } = this.state;
return (
<div>
<div
onClick={this.handleDialogClick}
style={{
display: visible ? 'block' : 'none',
position: 'fixed',
top: 100,
left: '50%',
marginLeft: -190,
width: 380,
height: 300,
background: '#fff'< div style = "box-sizing: border-box; color: RGB (0, 0, 0); line-height: 22px; font-size: 12px! Important; white-space: normal;"'close' : 'open'}</Button> </div> ); }}Copy the code
Isn’t it perfect? It’s almost flawless [cover one’s face]
But the actual effect is not what we want, clicking on the Dialog will still close.
You can make the following changes
1. What do you mean?
class Demo extends PureComponent {
state = {
visible: false};componentDidMount() {
document.body.addEventListener('click', (e) => {
if (e.target && (e.target.matches('.dialog') || e.target.matches('.btn'))) {
return;
}
this.setState({
visible: false}); }); }componentWillUnmount() {
document.body.removeEventListener('click'); } handleBtnClick = (e) => { const { visible } = this.state; this.setState({ visible: ! visible, }); }render() {
const { visible } = this.state;
return (
<div>
<div
className="dialog"
style={{
display: visible ? 'block' : 'none',
position: 'fixed',
top: 100,
left: '50%',
marginLeft: -190,
width: 380,
height: 300,
background: '#fff'< div> <Button onClick={this.handlebtnclick} className="btn">{visible ? 'close' : 'open'}</Button> </div> ); }}Copy the code
2. Use native events only
class Demo extends PureComponent {
state = {
visible: false};componentDidMount() {
document.body.addEventListener('click', (e) => {
if (e.target && e.target.matches('.dialog')) {
return;
}
this.setState({
visible: false}); }); document.querySelector('.btn').addEventListener('click', (e) => {
e.preventDefault();
e.cancelBubble = true; const { visible } = this.state; this.setState({ visible: ! visible, }); }); }componentWillUnmount() {
document.body.removeEventListener('click');
document.querySelector('.dialog').removeEventListener('click');
}
render() {
const { visible } = this.state;
return (
<div>
<div
className="dialog"
style={{
display: visible ? 'block' : 'none',
position: 'fixed',
top: 100,
left: '50%',
marginLeft: -190,
width: 380,
height: 300,
background: '#fff'< div style = "box-sizing: border-box; color: RGB (50, 50, 50)"btn">{visible ? 'close' : 'open'}</Button> </div> ); }}Copy the code
You see here, you see something?
React event mechanism
React implements an event synthesis mechanism based on Virtual Dom. The registered events will synthesize a SyntheticEvent object. If you want to access the nativeEvent object, you can access the nativeEvent attribute. React event mechanism eliminates browser compatibility issues and maintains a consistent performance with native events.
Source code analysis
The entrance
packages/react-dom/src/events/ReactBrowserEventEmitter.js
/**
* Summary of `ReactBrowserEventEmitter` event handling:
*
* - Top-level delegation is used to trap most native browser events. This
* may only occur in the main thread and is the responsibility of
* ReactDOMEventListener, which is injected and can therefore support
* pluggable event sources. This is the only work that occurs in the main
* thread.
*
* - We normalize and de-duplicate events to account for browser quirks. This
* may be done in the worker thread.
*
* - Forward these native events (with the associated top-level type used to
* trap it) to `EventPluginHub`, which in turn will ask plugins if they want
* to extract any synthetic events.
*
* - The `EventPluginHub` will then process each event by annotating them with
* "dispatches", a sequence of listeners and IDs that care about that event.
*
* - The `EventPluginHub` thendispatches the events. * * Overview of React and the event system: * * +------------+ . * | DOM | . * +------------+ . * | . * v . * +------------+ . * | ReactEvent | . * | Listener | . * +------------+ . +-----------+ * | . +--------+|SimpleEvent| * | . | |Plugin | * +-----|------+ . v +-----------+ * | | | . +--------------+ +------------+ * | +-----------.--->|EventPluginHub| | Event | * | | . | | +-----------+ | Propagators| * | ReactEvent | . | | |TapEvent | |------------| * | Emitter | . | |<---+|Plugin | |other plugin| * | | . | | +-----------+ | utilities | * | +-----------.--->| | +------------+ * | | | . +--------------+ * +-----|------+ . ^ +-----------+ * | . | |Enter/Leave| * + . +-------+|Plugin | * +-------------+ . +-----------+ * | application | . * |-------------| . * | | . * | | . * +-------------+ . * . * React Core . General Purpose Event Plugin System */Copy the code
Browse through the implementation of the event mechanism in the order of the flowchart
Event registration and storage
It all begins here…
packages/react-dom/src/client/ReactDOMComponent.js
ReactDOMComponent will walk through the Props of ReactNode and set a series of properties for the real DOM object to be rendered, including event registration.
// function diffProperties
if (registrationNameModules.hasOwnProperty(propKey)) {
if(nextProp ! = null) {// Exception when the event has not been delegatedif(__DEV__ && typeof nextProp ! = ='function') { warnForInvalidEventListener(propKey, nextProp); } // Props ensureListeningTo(rootContainerElement, propKey); } / /... }Copy the code
Event delegate, all events end up being delegated to a document or fragment
functionEnsureListeningTo (rootContainerElement: Element | Node, registrationName: string, / / registrationName: coming onClick) : void { const isDocumentOrFragment = rootContainerElement.nodeType === DOCUMENT_NODE || rootContainerElement.nodeType === DOCUMENT_FRAGMENT_NODE; Document const doc = isDocumentOrFragment? rootContainerElement : rootContainerElement.ownerDocument; listenTo(registrationName, doc); }Copy the code
Continue looking at listenTo’s code
export functionlistenTo( registrationName: string, mountAt: Document | Element | Node, ): void { const listeningSet = getListeningSetForElement(mountAt); / / registrationNameDependencies stores React event name and native browser event name corresponding to a Map of const dependencies = registrationNameDependencies[registrationName];for (leti = 0; i < dependencies.length; i++) { const dependency = dependencies[i]; // Call this method to register a listenToTopLevel(dependency, mountAt, listeningSet); }}Copy the code
ListenToTopLevel method
export function listenToTopLevel(
topLevelType: DOMTopLevelEventType,
mountAt: Document | Element | Node,
listeningSet: Set<DOMTopLevelEventType | string>,
): void {
if(! listeningSet.has(topLevelType)) { switch (topLevelType) {case TOP_SCROLL:
// trapCapturedEvent captures eventstrapCapturedEvent(TOP_SCROLL, mountAt);
break;
case TOP_FOCUS:
case TOP_BLUR:
trapCapturedEvent(TOP_FOCUS, mountAt);
trapCapturedEvent(TOP_BLUR, mountAt);
// We set the flag for a single dependency later in this function,
// but this ensures we mark both as attached rather than just one.
listeningSet.add(TOP_BLUR);
listeningSet.add(TOP_FOCUS);
break;
case TOP_CANCEL:
case TOP_CLOSE:
if (isEventSupported(getRawEventName(topLevelType))) {
trapCapturedEvent(topLevelType, mountAt);
}
break;
case TOP_INVALID:
case TOP_SUBMIT:
caseTOP_RESET: // listens on the target DOM elementbreak; // By default, listen for all non-media events at the top level. Media events don't bubble, so adding listeners doesn't do anything const isMediaEvent = MediaEventTypes.indexof (topLevelType)! = = 1;if(! isMediaEvent) { //trapBubbledEvent bubblingtrapBubbledEvent(topLevelType, mountAt);
}
break; } listeningSet.add(topLevelType); }}Copy the code
Capture event && Event bubble
// Capture the eventexport function trapCapturedEvent(
topLevelType: DOMTopLevelEventType,
element: Document | Element | Node,
): void {
trapEventForPluginEventSystem(element, topLevelType, true); } // Event bubbleexport function trapBubbledEvent(
topLevelType: DOMTopLevelEventType,
element: Document | Element | Node,
): void {
trapEventForPluginEventSystem(element, topLevelType, false);
}
function trapEventForPluginEventSystem(
element: Document | Element | Node,
topLevelType: DOMTopLevelEventType,
capture: boolean, // capture trueCapture,falseBubble): void {//...if(capture) {// capture the event addEventCaptureListener(Element, rawEventName, Listener); }else{// bubble addEventBubbleListener(element, rawEventName, listener); }}export function addEventCaptureListener(
element: Document | Element | Node,
eventType: string,
listener: Function,
): void {
element.addEventListener(eventType, listener, true);
}
Copy the code
The event is registered, and then what?
synthesize
Continue with EventPluginHub, which manages and registers plug-ins. The React event system uses a plug-in mechanism to manage events with different behaviors. These plug-ins process the corresponding types of events and generate composite event objects.
The following plug-ins are registered with EventPluginHub when ReactDOM starts
// packages/react-dom/src/client/ReactDOMClientInjection.js
EventPluginHubInjection.injectEventPluginsByName({
SimpleEventPlugin: SimpleEventPlugin,
EnterLeaveEventPlugin: EnterLeaveEventPlugin,
ChangeEventPlugin: ChangeEventPlugin,
SelectEventPlugin: SelectEventPlugin,
BeforeInputEventPlugin: BeforeInputEventPlugin,
});
Copy the code
1, the packages/react – dom/SRC/events/ChangeEventPlugin. Js
The Change event is a custom React event designed to normalize the change event of a form element. It supports these form elements: INPUT, textarea, and SELECT
2, packages/react – dom/SRC/events/EnterLeaveEventPlugin js
MouseEnter mouseLeave and pointerEnter pointerLeave are two special types of events
3, packages/react – dom/SRC/events/SelectEventPlugin js
Like the change event, React normalizes the SELECT event for form elements for input, Textarea, and contentEditable elements.
4, packages/react – dom/SRC/events/SimpleEventPlugin js
Simple events that handle some of the more general event types
5, packages/react – dom/SRC/events/BeforeInputEventPlugin js
Beforeinput event
Under the analysis SimpleEventPlugin
/**
* Turns
* ['abort'. ] * into * eventTypes = { *'abort': {
* phasedRegistrationNames: {
* bubbled: 'onAbort',
* captured: 'onAbortCapture',
* },
* dependencies: [TOP_ABORT],
* },
* ...
* };
* topLevelEventsToDispatchConfig = new Map([
* [TOP_ABORT, { sameConfig }],
* ]);
*/
Copy the code
// Generate a composite event, every plugin has this function extractEvents:function(
topLevelType: TopLevelType,
eventSystemFlags: EventSystemFlags,
targetInst: null | Fiber,
nativeEvent: MouseEvent,
nativeEventTarget: EventTarget,
): null | ReactSyntheticEvent {
const dispatchConfig = topLevelEventsToDispatchConfig[topLevelType];
if(! dispatchConfig) {returnnull; } / /... / / remove this event object from the pool of instances of a const event = EventConstructor. GetPooled (dispatchConfig targetInst, nativeEvent, nativeEventTarget, ); accumulateTwoPhaseDispatches(event);return event;
}
Copy the code
EventPropagators
/ / packages/legacy - events/EventPropagators js / / this function is used to synthesize events with the listener, in the end all of the same type listener will be put into _dispatchListenersfunction accumulateDirectionalDispatches(inst, phase, event) {
if (__DEV__) {
warningWithoutStack(inst, 'Dispatching inst must not be null'); } const Listener = listenerAtPhase(inst, event, phase);if(listener) {// Place all listeners into _dispatchListeners // _dispatchListeners = [onClick, outClick] event._dispatchListeners = accumulateInto( event._dispatchListeners, listener, ); event._dispatchInstances = accumulateInto(event._dispatchInstances, inst); } // Find the callback function listener for the different phases (capture/bubble) element bindingfunction listenerAtPhase(inst, event, propagationPhase: PropagationPhases) {
const registrationName = event.dispatchConfig.phasedRegistrationNames[propagationPhase];
return getListener(inst, registrationName);
}
Copy the code
// packages/legacy-events/EventPluginHub.js
/**
* @param {object} inst The instance, which is the source of events.
* @param {string} registrationName Name of listener (e.g. `onClick`).
* @return {?function} The stored callback.
*/
export function getListener(inst: Fiber, registrationName: string) {
let listener;
// TODO: shouldPreventMouseEvent is DOM-specific and definitely should not
// live here; needs to be moved to a better place soon
const stateNode = inst.stateNode;
if(! stateNode) { // Workin progress (ex: onload events in incremental mode).
return null;
}
const props = getFiberCurrentPropsFromNode(stateNode);
if(! props) { // Workin progress.
return null;
}
listener = props[registrationName];
if (shouldPreventMouseEvent(registrationName, inst.type, props)) {
return null;
}
invariant();
return listener;
}
Copy the code
Summary: Composite events that collect a wave of callback functions of the same type as click exist in event._DispatchListeners
Event distribution and execution
For events registered on the Document, the corresponding callback function fires the dispatchEvent method, which is the entry method for event distribution.
export functionDispatchEvent (topLevelType: DOMTopLevelEventType, // The name of the event with top, such as topClick. EventSystemFlags: eventSystemFlags, nativeEvent: AnyNativeEvent, // nativeEvent passed by the browser when the user triggers click): void {if(! _enabled) {return;
}
if(hasQueuedDiscreteEvents () && isReplayableDiscreteEvent (topLevelType)) {/ / already has an event queue, QueueDiscreteEvent (NULL, topLevelType, eventSystemFlags, nativeEvent,);return;
}
const blockedOn = attemptToDispatchEvent(
topLevelType,
eventSystemFlags,
nativeEvent,
);
if (blockedOn === null) {
// We successfully dispatched this event.
clearIfContinuousEvent(topLevelType, nativeEvent);
return;
}
if (isReplayableDiscreteEvent(topLevelType)) {
// This this to be replayed later once the target is available.
queueDiscreteEvent(blockedOn, topLevelType, eventSystemFlags, nativeEvent);
return;
}
if (
queueIfContinuousEvent(
blockedOn,
topLevelType,
eventSystemFlags,
nativeEvent,
)
) {
return; } // Since queuing is cumulative, clearIfContinuousEvent(topLevelType, nativeEvent) needs to be cleared only if there is no queuing; //in case the event system needs to trace it.
if (enableFlareAPI) {
if (eventSystemFlags & PLUGIN_EVENT_SYSTEM) {
dispatchEventForPluginEventSystem(
topLevelType,
eventSystemFlags,
nativeEvent,
null,
);
}
if(eventSystemFlags & RESPONDER_EVENT_SYSTEM) { // React Flare event system dispatchEventForResponderEventSystem( (topLevelType: any), null, nativeEvent, getEventTarget(nativeEvent), eventSystemFlags, ); }}else{ dispatchEventForPluginEventSystem( topLevelType, eventSystemFlags, nativeEvent, null, ); }}functiondispatchEventForPluginEventSystem( topLevelType: DOMTopLevelEventType, eventSystemFlags: EventSystemFlags, nativeEvent: AnyNativeEvent, targetInst: null | Fiber, ): void { const bookKeeping = getTopLevelCallbackBookKeeping( topLevelType, nativeEvent, targetInst, eventSystemFlags, ); Try {// allow event queues to be processed in the same cycle // preventDefault preventDefault batchedEventUpdates(handleTopLevel, bookKeeping); } finally { releaseTopLevelCallbackBookKeeping(bookKeeping); }}Copy the code
functiondispatchEventForPluginEventSystem( topLevelType: DOMTopLevelEventType, eventSystemFlags: EventSystemFlags, nativeEvent: AnyNativeEvent targetInst: null | Fiber,) : void {/ / bookKeeping is used to store will use to the variables in the process of object. The initialization uses the object pooling method that react uses in the source code to avoid unnecessary garbage collection, const bookKeeping = getTopLevelCallbackBookKeeping( topLevelType, nativeEvent, targetInst, eventSystemFlags, ); Try {// allow event queues to be processed in the same cycle // preventDefault preventDefault batchedEventUpdates(handleTopLevel, bookKeeping); } finally { releaseTopLevelCallbackBookKeeping(bookKeeping); }}Copy the code
The core of event distribution is to distribute events using batch processing. HandleTopLevel is the real executor of event distribution. It does two things. First, it constructs React composite events from native events sent back by the browser, and second, it queues events.
function handleTopLevel(bookKeeping: BookKeepingInstance) {
lettargetInst = bookKeeping.targetInst; // Iterate over the hierarchy in case there are any nested components. // It is important that we establish the parent array // event handlers before calling any of the ancestors, because event handlers can modify the DOM, resulting in an incompatibility with ReactMount's node cache.letancestor = targetInst; The execution of the event callback function may cause changes to the Virtual DOM structure. // Before execution, store the DOM structure when the event is triggereddo {
if(! ancestor) { const ancestors = bookKeeping.ancestors; ((ancestors: any): Array<Fiber | null>).push(ancestor);break;
}
const root = findRootContainerNode(ancestor);
if(! root) {break;
}
const tag = ancestor.tag;
if (tag === HostComponent || tag === HostText) {
bookKeeping.ancestors.push(ancestor);
}
ancestor = getClosestInstanceFromNode(root);
} while(ancestor); // You can't stop bubbles by stopPropagation. // You can't stop bubbles by stopPropagation.for (leti = 0; i < bookKeeping.ancestors.length; i++) { targetInst = bookKeeping.ancestors[i]; // DOM const eventTarget = getEventTarget(bookKeeping. NativeEvent); const topLevelType = ((bookKeeping.topLevelType: any): DOMTopLevelEventType); Const nativeEvent = ((bookKeeping. NativeEvent: any): AnyNativeEvent); runExtractedPluginEventsInBatch( topLevelType, targetInst, nativeEvent, eventTarget, bookKeeping.eventSystemFlags, ); }}Copy the code
React implements a bubbling mechanism that starts with the object that triggered the event, and then traces back to the parent element, calling the event callbacks that they registered in turn.
conclusion
The event handler we defined in React receives an example of a synthesized event object (using nativeEvent to access the nativeEvent object). React eliminates the problem of compatibility across different browsers by having the same interface as native browser events and the same support for bubbling. Try stopPropagation() and preventDefault() to stop it. Except for some media events (such as onPlay onPause), React does not bind events directly to real nodes. Instead, React proxies events to documents.