This article explains react’s synthetic event mechanism. When it comes to composite events, some people may think that React composite events simply give us a browser-compatible event object. But it’s not as simple as that. For example, look at the following code output (click on inner) :

class App extends React.Component {
  componentDidMount() {
    outer.addEventListener('click'.function() {
      console.log('native event click outer')
    })
    
    inner.addEventListener('click'.function() {
      console.log('native event click inner')
    })
  }
  
  handleClickInner = () = > {
    console.log('react event click inner')
  }
  
  handleClickOuter = () = > {
    console.log('react event click outer')}render() {
    return (
    	<div className='outer' onClick={this.handleClickInner}>
      	<div className='inner' onClick={this.handleClickOuter}></div>
      </div>)}}Copy the code

Native event click inner -> native event click outer -> React event click inner -> React event click outer

If you don’t know why, this article will help.

Here’s a look at react’s synthetic event mechanism

General introduction

First of all, React’s synthetic event mechanism does more than encapsulate event objects that are compatible with browsers. It implements its own event-triggering mechanism. First we need to clarify two concepts: React events and native events.

React events are code we write in JSX like onClick={this.handleclick}. Events added using addEventListener are native events. React only processes react events using a synthetic event mechanism, not native events.

React event processing is divided into two phases: top-level registration for initial rendering, and simulated dispatch when the event is triggered.

The top register

The React composite event mechanism uses the idea of event proxies, binding native events that each React event depends on to the root element. This refers to the native events that react events depend on. The react event: onMouseEnter, for example, relies on two native events: mouseout and mouseover. React binds all native events that react events depend on to the root element, and both capture and bubble events are bound. Events bound to the root element can be seen in Chrome’s developer tools.

The mount of these native events is mounted in reactdom.render. ReactDOM will render call createRootImpl method, this method will be called listenToAllSupportedEvents method, cycle all events, by intercepting addTrappedEventListener function.

Now there is a question: what is the listener function? In fact, the event listener on the root element is a trigger, and the trigger is created in the addTrappedEventListener function

function addTrappedEventListener(targetContainer, domEventName, eventSystemFlags, isCapturePhaseListener, isDeferredListenerForLegacyFBSupport) {
  var listener = createEventListenerWrapperWithPriority(targetContainer, domEventName, eventSystemFlags);
  // ... 
}

Copy the code

CreateEventListenerWrapperWithPriority can be seen from the name, this method creates a trigger with a priority

function createEventListenerWrapperWithPriority(targetContainer, domEventName, eventSystemFlags) {
  var eventPriority = getEventPriorityForPluginSystem(domEventName);
  var listenerWrapper;

  switch (eventPriority) {
    case DiscreteEvent:
      listenerWrapper = dispatchDiscreteEvent;
      break;

    case UserBlockingEvent:
      listenerWrapper = dispatchUserBlockingUpdate;
      break;

    case ContinuousEvent:
    default:
      listenerWrapper = dispatchEvent;
      break;
  }

  return listenerWrapper.bind(null, domEventName, eventSystemFlags, targetContainer);
}
Copy the code

Regardless of the priority, the dispatchEvent function will eventually be called, so this function is the final trigger.

React completes the top-level registration of events. When we trigger the click event, we enter the simulated dispatch phase.

Distributed simulation

If we fire a click event, first in the capture phase, according to the DOM event flow, the capture phase listener function of the root element’s click event binding will be fired, namely the dispatchEvent function. The dispatchEvent function determines the event source object based on the event.target property and collects all DOM nodes from the source object to the fiber root node and bound event handlers, which are then called once, thus simulating the event capture phase.

In the event bubble phase, the execution process is similar. When the event bubbles to the root element, the dispatchEvent function is triggered, and then the event handlers are collected and executed in turn.

To be clear, if we attach an onClick event to a div, React does not bind the event to that div. Instead, react calls the div’s event handler to simulate the event.

Now that we know the general process, let’s look at the source code

DispatchEvent will call dispatchEventForPluginEventSystem function

function dispatchEventForPluginEventSystem(domEventName, eventSystemFlags, nativeEvent, targetInst, targetContainer) {
  // ...
  batchedEventUpdates(function () {
    return dispatchEventsForPlugins(domEventName, eventSystemFlags, nativeEvent, ancestorInst);
  });
}
Copy the code

I’m not going to look at batchedEventUpdates here, which is related to state updates, but it’s going to call dispatchEventsForPlugins.

function dispatchEventsForPlugins(domEventName, eventSystemFlags, nativeEvent, targetInst, targetContainer) {
  var nativeEventTarget = getEventTarget(nativeEvent);
  var dispatchQueue = [];
  // Event path collection
  extractEvents$5(dispatchQueue, domEventName, targetInst, nativeEvent, nativeEventTarget, eventSystemFlags);
  // Event simulation
  processDispatchQueue(dispatchQueue, eventSystemFlags);
}
Copy the code

The extractEvents$5 method calls extractEvents$4

function extractEvents$4(dispatchQueue, domEventName, targetInst, nativeEvent, nativeEventTarget, eventSystemFlags, targetContainer) {
  // ...
  var _listeners = accumulateSinglePhaseListeners(targetInst, reactName, nativeEvent.type, inCapturePhase, accumulateTargetOnly);
  
  if (_listeners.length > 0) {
    var _event = new SyntheticEventCtor(reactName, reactEventType, null, nativeEvent, nativeEventTarget);
    dispatchQueue.push({
      event: _event,
      listeners: _listeners }); }}Copy the code

AccumulateSinglePhaseListeners will collect event trigger path, and returns the collected event handler array. Then create a composite event object. That’s why the React documentation states that all events share one event object.

Look at the next accumulateSinglePhaseListeners

function accumulateSinglePhaseListeners(targetFiber, reactName, nativeEventType, inCapturePhase, accumulateTargetOnly) {
  varcaptureName = reactName ! = =null ? reactName + 'Capture' : null;
  var reactEventName = inCapturePhase ? captureName : reactName;
  var listeners = [];
  var instance = targetFiber;
  var lastHostComponent = null;

  while(instance ! = =null) {
    var _instance2 = instance,
        stateNode = _instance2.stateNode,
        tag = _instance2.tag;
    // HostComponent is a browser native label, such as div, button
    if(tag === HostComponent && stateNode ! = =null) {
      lastHostComponent = stateNode;

      if(reactEventName ! = =null) {
        var listener = getListener(instance, reactEventName);

        if(listener ! =null) { listeners.push(createDispatchListener(instance, listener, lastHostComponent)); }}}if (accumulateTargetOnly) {
      break;
    }

    instance = instance.return;
  }

  return listeners;
} 
Copy the code

The main body of this method is a loop, starting with the fiber event source and looking upwards. If you encounter a HostComponent, collect the event listener for the fiber object and place it into an array of listeners. Note that this method only collects event handlers for one of the capture and bubble phases. Event functions in the capture phase are collected after dispatchEvent is triggered in the capture phase, and event functions in the bubble phase are collected after dispatchEvent is triggered in the bubble phase.

After the event functions collect, began to simulate trigger, call processDispatchQueue method, this method will be called processDispatchQueueItemsInOrder listeners array to cycle.

function processDispatchQueueItemsInOrder(event, dispatchListeners, inCapturePhase) {
  var previousInstance;

  if (inCapturePhase) {
    for (var i = dispatchListeners.length - 1; i >= 0; i--) {
      var _dispatchListeners$i = dispatchListeners[i],
          instance = _dispatchListeners$i.instance,
          currentTarget = _dispatchListeners$i.currentTarget,
          listener = _dispatchListeners$i.listener;

      if(instance ! == previousInstance && event.isPropagationStopped()) {return; } executeDispatch(event, listener, currentTarget); previousInstance = instance; }}else {
    for (var _i = 0; _i < dispatchListeners.length; _i++) {
      var _dispatchListeners$_i = dispatchListeners[_i],
          _instance = _dispatchListeners$_i.instance,
          _currentTarget = _dispatchListeners$_i.currentTarget,
          _listener = _dispatchListeners$_i.listener;

      if(_instance ! == previousInstance && event.isPropagationStopped()) {return; } executeDispatch(event, _listener, _currentTarget); previousInstance = _instance; }}}Copy the code

This method is also a loop. Note that the sequence of functions in our Listeners array, from the event source object to the top layer fiber, corresponds to the sequence in the bubbling phase. Therefore, if the current phase is the capture phase, then the array is traversed in reverse, if the bubble phase, forward.

The executeDispatch method executes the execution event callback function.

Again, for a simulated dispatch process, it is executed once during the event capture phase and again during the bubble phase.

An 🌰

Going back to the example at the beginning of this article, click on inner. What is the output?

class App extends React.Component {
  componentDidMount() {
    outer.addEventListener('click'.function() {
      console.log('native event click outer')
    })
    
    inner.addEventListener('click'.function() {
      console.log('native event click inner')
    })
  }
  
  handleClickInner = () = > {
    console.log('react event click inner')
  }
  
  handleClickOuter = () = > {
    console.log('react event click outer')}render() {
    return (
    	<div className='outer' onClick={this.handleClickInner}>
      	<div className='inner' onClick={this.handleClickOuter}></div>
      </div>)}}Copy the code

Step by step: On the first render, react events and native events are registered. When the inner is clicked, the event capture phase is first entered, the click event of the root element is triggered, and the dispatchEvent of the capture phase is triggered, but the listener function for the capture phase is not bound, so it is skipped directly.

Next, you enter the target stage, which triggers the native Click event of the inner and outputs native Event click Inner.

Then we go to the bubbling phase, we go to the Outer, we fire the native event for the Outer, we print native Event click outer. Then, we start to collect the path of the event trigger, and collect the collected array of handleClickInner, handleClickOuter. React Event click inner, React Event click Outer

Native event click inner -> native event click outer -> React event click inner -> React event click outer

conclusion

This article explains the two principal processes of the React event mechanism, top-level registration and mock dispatch. I believe you can also understand the meaning of the word simulation. One function not covered in this article is batchedEventUpdates, which covers status updates, and there will be a later article on combining event firing with status updates.