“This article has participated in the good article call order activity, click to see: back end, big front end double track submission, 20,000 yuan prize pool for you to challenge!”

Studied the React the classmate’s friends all know that the React system has its own events, and DOM native event system is not the same, but what is different, and what is the difference between native DOM events and contact, it’s a hard question to answer, because he was involved in the amount of code that many, relevant code logic nor cohesion, The overall difficulty of understanding is difficult. I was confused by him once. Later, by grasping the main logic flow of React event characteristics, I can be regarded as the whole Event system of React. Today we will discuss it.

Let’s first review what the native DOM event stream looks like, and then find the source of inspiration for React events based on the event delegate of native events. Then we’ll explore how the React event system works and take it apart to understand the collection and execution of event callbacks. In this way, we can understand the whole event of React step by step. Finally, let’s think about the reasons and motivations for the design of React event system, and what factors prompted the React team to design it like this. The structure and design of the whole article is also in accordance with the above ideas. I hope to bring new thinking and inspiration to you through this article.

All the code and research foundation in this article are based on 16.13.0. The next article will study the event system on React 17. Welcome to follow me

Native DOM event stream

We implement the interaction between JS and HTML in the browser through event listening. A page is often bound to many events, the order of events received by the page, is the event flow. The W3C standard specifies that the propagation of an event goes through three stages:

  • Event capture phase
  • The target stage
  • Event bubble phase

Let’s look at a graph

When an event is triggered, it goes through a process of capture. The event “passes” from the outermost element to the innermost element, and the process continues until the event reaches its target element (the element that actually triggers the event). At this point the time stream switches to the “target phase” (where the event is received by the target element); The event will then “bounce back” into the bubble phase, “going upstream” on the way it came, layer by layer, and back again.

It’s a little bit like the trampoline we play on, jumping on the bed and bouncing up, and the whole thing is a V.

Event delegation

In native DOM, event delegate is an important means of performance optimization. Events of child nodes are captured through E.t. Get by means of event bubble. Look at the following code:

<! DOCTYPEhtml>
<html lang="en">
<head>
  <meta charset="UTF-8">
  <meta name="viewport" content="Width = device - width, initial - scale = 1.0">
  <meta http-equiv="X-UA-Compatible" content="ie=edge">
  <title>Document</title>
</head>
<body>
  <ul id="list">
    <li>1</li>
    <li>2</li>
    <li>3</li>
    <li>4</li>
    <li>5</li>
    <li>6</li>
    <li>7</li>
    <li>8</li>
    <li>9</li>
    <li>0</li>
  </ul>
</body>
</html>
Copy the code

In this code, if you want to be able to click on each li element and output the contents of the text, what do you do?

The most straightforward is to bind each li element to a listener event, which is written like this:

<script>

  // Get the list of li

  var liList = document.getElementsByTagName('li')

  // Install the listening functions one by one

  for (var i = 0; i < liList.length; i++) {

    liList[i].addEventListener('click'.function (e) {.console.log(e.target.innerHTML)

    })

  }

</script>

Copy the code

But it’s not very elegant, and it’s not very good performance, and if there are hundreds of nodes, and there are hundreds of nodes listening for events, how do you solve this problem? We can use the event bubble!

For each of the 10 Li elements, no matter which li the click takes place on, the click event will eventually be bubbling up to their common father, ul, so ul can help perceive the click event.

Since UL is aware of events, it can help handle them, because we have e.t. Get. Ul can delegate to the element that actually triggers the event using the target attribute in the event object.

With that in mind, we can get rid of the for loop. Here is the code to do the same with the event agent:

var ul = document.getElementById('poem')

ul.addEventListener('click'.function(e){

  console.log(e.target.innerHTML)

})

Copy the code

The e.t. get attribute in the code refers to the specific target that triggers the event and records the source of the event. So here, no matter what level the listener is executing, as long as we get THE e.t. get, we get the actual triggered element. After we get the element, we can completely simulate its behavior and achieve the undifferentiated listening effect.

Using the bubble feature of events, the same type of listening logic of multiple child elements is merged into the parent element and managed by a listening function, which is event delegate. Through event delegate, memory overhead can be reduced, registration steps can be simplified, and development efficiency can be greatly improved.

This way of delegating events was the inspiration for React’s composite events. Let’s take a look at how the React event system works.

How does the React event system work

The React event system adopts the idea of event delegation. In React, except for a few special non-bubble events (such as media type events) that cannot be processed by the event system, most events are not bound to specific elements, but are bound to the document of the page. When an event is triggered on a specific DOM node, All of this will eventually bubble up to the Document, where the unified event handler bound to the document will distribute the event to the specific component instance.

So, React first wraps the event, wrapping native DOM events into composite events, before distributing them.

What is a React composite event

Composite event is a custom event object defined by React. It is a composite event defined according to the W3C specification, which smooths the differences between different browsers on the bottom layer and exposes the unified, stable and same event interface as DOM native events to upper-layer developers. Instead of focusing on cumbersome compatibility issues, developers can focus on developing business logic.

Although the synthesized event is not a native DOM event, a reference to the native DOM event is retained. If you need to access the native DOM event object, you can obtain the native DOM event reference by synthesizing the e.nativeEvent property of the event object, as shown below:

E.nativeevent outputs a reference to the MouseEvent native event

We roughly talked about the React event system can basic principle, the basic concept of synthetic events have a certain understanding, then combined with React source code and call stack, the workflow of the event system for in-depth understanding, refuze, hard to bite the bone!

How the React event system works

The event system never jumps out of the two main actions of “event binding” and “event triggering.” Let’s look at how event binding is implemented.

event

The binding of the event is done during the mounting process of the component (completeWork), during which there are three main actions

  • Creating a DOM Node
  • Insert the DOM node into the DOM tree
  • Set DOM node properties

In the process of setting node properties for DOM, the props of FiberNode are traversed. When the props related to the event are traversed, the registration link of the event is triggered. Complete source code can refer to this address.

Considering that the source code may change slightly over time, I have taken the code out and deleted many of the things that affect understanding. The code is as follows:

function completeWork(current, workInProgress, renderLanes) {
  // Retrieve the properties of the Fiber node and store them in newProps
  const newProps = workInProgress.pendingProps;
  // The workInProgress node's tag attribute is different from the workInProgress node's tag attribute.
  // Let's focus on HostComponent and delete everything else
  switch (workInProgress.tag) {
    // There are many cases, but for the sake of understanding, we only keep the HostComponent case
    case HostComponent: {
      // Dom node logic
      popHostContext(workInProgress);
      const rootContainerInstance = getRootHostContainer();
      const type = workInProgress.type;
      // Check whether the current node exists. During the mount phase, the current node does not exist.
      // This logic is only used when an update is triggered. We will talk about the rendering and updating logic later
      if(current ! = =null&& workInProgress.stateNode ! =null) {
        updateHostComponent(
          current,
          workInProgress,
          type,
          newProps,
          rootContainerInstance
        );
        if (current.ref !== workInProgress.ref) {
          markRef(workInProgress);
        }
      } else {
        // Prepare for Dom node creation
        const currentHostContext = getHostContext();
        // Ignore values related to server rendering
        const wasHydrated = popHydrationState(workInProgress);
        // Determine whether the server is rendering or not
        if (wasHydrated) {
          if( prepareToHydrateHostInstance( workInProgress, rootContainerInstance, currentHostContext ) ) { markUpdate(workInProgress); }}else {
          // Create a DOM node
          const instance = createInstance(
            type,
            newProps,
            rootContainerInstance,
            currentHostContext,
            workInProgress
          );
          // Try to mount the Dom node created in the previous step to the Dom tree.
          It's possible that the DOM tree doesn't exist yet, but that's ok
          appendAllChildren(instance, workInProgress, false.false);
          // stateNode stores the DOM node corresponding to the current Fiber node.
          // Here each node holds its own DOM instance,
          // So it doesn't matter if the DOM tree doesn't exist yet,
          // Pull the DOM node from FiberNode when the DOM exists
          workInProgress.stateNode = instance;
          // This is used to set properties for the DOM node.
          // In fact, the return value is assigned to a variable, it is easier to understand, but the source code is a lot of this way to write, uncomfortable
          if( finalizeInitialChildren( instance, type, newProps, rootContainerInstance, currentHostContext ) ) { markUpdate(workInProgress); }}if(workInProgress.ref ! = =null) {
          markRef(workInProgress);
        }
      }
      bubbleProperties(workInProgress);
      return null; }}}Copy the code

At present, from the source code, the general flow chart is as follows:

Combined with the source code and flow chart, the next focus is in finalizeInitialChildren, but this is too uncomfortable, and to jump to finalizeInitialChildren source code, which is completely nesting dolls, our purpose is mainly to sort out the whole process, Instead of being trapped in the source code can not extricate themselves, eventually let oneself see the code to doubt life. So I’m going to share a method here today to see the whole call flow.

  1. withcreate-react-appCreate a React project and replace it with the following code
import { useState } from "react";

function App() {
  const [count, setCount] = useState(0);

  return (
    <div>
      <div
        onClick={(e)= >{console.log(" native DOM events are: ", e.nativeEvent); setCount(count + 1); }} ><p>{count}</p>
      </div>
    </div>
  );
}

export default App;
Copy the code
  1. Open Chrome’s Performance panel and click the refresh button (circled).

  1. Wait 3-4 seconds, then terminate the record, and you get the following call stack diagram.

  1. And then we foundcompleteWork, you can look at the call stack.

Finally, looking at the overall call stack diagram, we can rearrange our flowchart, which is much simpler than looking at the source code.

As you can see, the event registration process is enabled by the ensureListeningTo function, which attempts to retrieve the root node (the document object) in the current DOM structure. Then register the uniform event listener function on the Document by calling legacyListenToEvent. Register the uniform event listener function with the Document.

In legacyListenToEvent, is, in fact, by calling the legacyListenToTopLevelEvent to handle the relationship between the event and the document, from TopLevelEvent can see that listening is the top event, This top level can be thought of as the top level of the event delegate, which is the Document node.

In legacyListenToTopLevelEvent one point to note that the picture I ring out, there is a listenerMap variables, using the map as a data storage structure, stored in the event of the entire document.

Here, legacyListenToTopLevelEvent is the starting point of the whole event, will first determine listenerMap inside to have this event, but look, here topLevelType refers to the function of the context on behalf of the event types, for example, if the click event, So the value of topLevelType is click. As shown in the figure below:

Another detail that’s worth noticing is why we’re using a Map to store the Document event, and we’re using topLevelType as the map key. If you think about it, this is actually an optimization of React. Even if we call the listen for the same event multiple times in the React project, it will only trigger one registration on the Document. And what React ends up registering on the Document is not a specific callback logic on a DOM node, but just a distribution function

Let’s take a break on the addEventBubbleListener to find out!

Element is a document, eventType is click, and listener is a dispatchDiscreteEvent function. There are no discreteEvents for the listener. There are no discrete events for the listener. For dispatchDiscreteEvent, see the code for “dispatchDiscreteEvent”.

function dispatchDiscreteEvent(topLevelType, eventSystemFlags, container, nativeEvent) {
  flushDiscreteUpdatesIfNeeded(nativeEvent.timeStamp);
  discreteUpdates(dispatchEvent, topLevelType, eventSystemFlags, container, nativeEvent);
}
Copy the code

Through the source code of dispatchDiscreteEvent, we can see that the final binding to the document on the unified event distribution function, in fact, is dispatchEvent

So the question is, how does dispatchEvent implement event distribution?

Triggering of events

The event trigger is basically the call of dispatchEvent, but the triggering link of dispatchEvent is quite long, and many students should be estimated by the above process to be completely dizzy, so I directly draw the whole core workflow.

The first three steps have been analyzed above, but what is new to us are steps 4, 5 and 6, which are also the three steps we should focus on.

Let’s do a Demo to understand how this works

import React, { useState } from "react";

function App() {
  const [value, setValue] = useState(0);
  return (
    <div
      onClickCapture={()= >Console. log(" capture past div")} onClick={() => console.log(" Bubble past div")} ><p>{value}</p>
      <button
        onClick={()= >{ setValue(value + 1); }} > plus one</button>
    </div>
  );
}
export default App;
Copy the code

So this Demo looks like this

Demo is a line of numeric text and a button, and with each click of the button, the numeric text increases by 1. In the JSX structure, in addition to the button, there is a div tag that listens for both the bubbling and the capture of click events.

The Fiber tree structure for the whole Demo looks like this

This Fiber tree structure is used to understand the event callback collection process

function traverseTwoPhase(inst, fn, arg) {
  // Define an array of paths to store captured and bubbled nodes
  var path = [];

  while (inst) {
    // Collect the current node into the path array
    path.push(inst);
    // Collect the parent node of the HostComponent tag ===
    inst = getParent(inst);
  }

  var i;
  // Collect the nodes involved in the capture process and the corresponding callbacks from the path array
  for (i = path.length; i-- > 0;) {
    fn(path[i], 'captured', arg);
  }
  // Collect the nodes in the path array that participate in the bubbling process and the corresponding callbacks
  for (i = 0; i < path.length; i++) {
    fn(path[i], 'bubbled', arg); }}Copy the code

From the code, the traverseTwoPhase function does three things:

  1. Loop to collect eligible parent nodes and store them in the PATH array

TraverseTwoPhase starts with the current node (the target node of the event) and continues to look up for the parent of the Tag === HostComponent, collecting them in order into the path array. If you don’t know why you only collect HostComponent tag ===, HostComponent refers to the Fiber node type of the DOM element. In the browser, DOM events are propagated only between DOM nodes, and there is no point in collecting other nodes.

Let’s look at the contents of the path array

The last thing to collect is the Button node itself (the starting point of the while loop, which will be pushed in the first place) and the div node.

  1. Simulate the propagating sequence of events during the capture phase, collect node instances and callback functions related to the capture phase

The traverseTwoPhase traverses the PATH array backwards, simulating the capture sequence of events, and collecting callback and instances of events that correspond to the capture phase.

The last little thing we said was that the PATH array starts from the target node and goes up, so the path array has the neutron node in front and the ancestor node in back, and this is the same thing as the DOM node bubble phase.

The process of traversing the path array from the parent node down to the child node until the target node is traversed in the same order as DOM events propagated in the capture node. 参 考 答 案 _DISPATchin参 考 (syntheticEvent._dispatchin参 考) During the entire iteration, the fn function checks the call-upcondition of a node one by one and if there is an event in the 参 考 instance on this node, the instance is collected in the _dispatchInstances (syntheticEvent._dispatchin参 考) event. Event callbacks are collected in the _dispatchListeners property of the composite event (syntheticEvent._DispatchListeners) and await subsequent execution. Let’s debug it with a breakpoint:

参 考 答 案 We can see that the instance of DIV is stored in the _Dispatchin参 考, and the callback events are stored in the _dispatchListeners, waiting for subsequent execution.

  1. Simulate the sequence of events propagating in the bubble phase, collect the bubble phase node instances and callback functions

After the capture phase is complete, the traverseTwoPhase traverses the path array backwards, simulating the bubble sequence of events, and collecting callback and instances of events corresponding to the capture phase.

The procedure is basically the same as step 2, except that the path array is now traversed in positive order instead of in reverse order. Positive sequence traversal also checks for the condition of the callback. If the bubble callback corresponding to the current time on the node is not empty, In this case, instances of the node and event call-ups will be collected in THE SYNtheticEvent._dispatchinpO and syntheticEvent._DispatchListeners respectively.

Therefore, in the execution phase of event callback, the whole DOM event flow can be simulated in one go by simply executing the callback functions in the SyntheticEvent._DispatchListeners array, which is also the “capture – target – bubble” phase.

conclusion

We learned the workflow of the React event system based on the native DOM event stream. One question that hasn’t been answered yet is that React simulates a DOM event stream based on synthetic events. What are the benefits and motivations for doing so? This question is often asked in the interview, so in this conclusion stage to comb through this question.

  1. Composite events comply with the W3C specification, smoothing the differences between different browsers at the bottom level, and providing developers with a unified, stable event interface with DOM native events at the top level. So developers can stop focusing on cumbersome underlying compatibility issues and focus on developing business logic.
  2. The self-developed event system allows React to take the initiative in event processing, just like there are mature frameworks like React and Vue, but each big factory is still developing its own framework to cover its own business scenarios. React kneads multiple events into one event, and the native DOM will not help it deal with it. The native focus is commonality, while React thinks that it can largely intervene the performance of events through its own event system, so that they can meet its own needs.

All the code and research foundation in this article are based on 16.13.0. The next article will study the event system on React 17. Welcome to follow me