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.