Special event systems: How are React events different from DOM events?

React has its own event system, which is different from the DOM native event system. But few people can say exactly what the difference is.

We know that different learning strategies need to be adopted for different knowledge. As for React event system, the amount of source code involved is not small, the related logic is not cohesive enough, the overall cost of understanding is relatively high, many people will be discouraged because of this.

Fortunately, React event-related questions, both in interview scenarios and in real development, tend to test the logic of event workflow, event characteristics, and other issues rather than the details of the source code. The main contradiction in logic, such as event workflow and event feature, is the focus of this lecture.

React 16.13.x As React versions change, the implementation details of the event system will inevitably change, but the design philosophy is always consistent, so the key is to grasp the core logic.

1. Review the event flow in the native DOM

At this stage of front-end development, there’s magic realism: some people talk a lot when they talk about front-end frameworks, but when they talk about DOM fundamentals, they start talking gibberish. But be aware: to understand the React event mechanism, you must have a solid grasp of the native DOM event flow. So let’s start with a quick review of how DOM event flow works.

In the browser, the interaction between JS and HTML is realized through event listening. A page is bound to many events, and the order in which the page receives events is the event stream.

The W3C standard states that the propagation of an event goes through three phases:

  • Event capture phase

  • The target stage

  • Event bubbling phase

You can understand this process by looking at the structure diagram of a DOM tree below, where the arrows represent the “shuttle” paths of events.

When an event is triggered, it first goes through a trapping process: the event “shuttles” from the outermost element to the innermost element until it reaches the element it is targeting (the element that actually triggered the event). At this point, the event flow switches to the “target phase” — events are received by the target element; The event then “bounces back” into the bubbling phase — it “goes upstream” the way it came, layer by layer, back again.

It’s a lot like when you were a kid on a trampoline: you fall, you hit the trampoline, you bounce back up, and you go in a symmetrical “V” shape.

2. Performance optimization thinking under DOM events: event delegate

Event delegation (also known as event broker) is an important performance optimization tool in the native DOM. Here is an interview question to quickly recall relevant knowledge.

Take a look at the following code:

<! DOCTYPE html> <html lang="en"> <head> <meta charset="UTF-8"> <meta name="viewport" content="width=device-width, <meta HTTP-equiv =" x-UA-compatible "content=" IE =edge"> <title>Document</title> </head> <body> <ul Id = "poem" > < li > bed Ming moonlight < / li > < li > doubt is the ground frost < / li > < li > look at the bright moon < / li > < li > bowed their heads and remembering home < / li > < li > hoe grain to be noon for day < / li > < li > began sweating grain soils < / li > < li > who knows it-and < / li > < li > grains are hard < / li > < li > back any < / li > < li > I back < / li > < / ul > < / body > < / HTML >Copy the code

Q: In this HTML rendering of the interface, we want to click on each li element to output its internal text content. What would you do?

An intuitive way of thinking about this is to let each li element listen for a click. The code that follows this idea looks like this:

Get the li list < script > / / var liList = document. The getElementsByTagName (' li ') / / install surveillance function for individually (var I = 0; i < liList.length; i++) { liList[i].addEventListener('click', function (e) { console.log(e.target.innerHTML) }) } </script>Copy the code

Of course, it is possible to install 10 listeners on 10 Li’s like this, but not only is it cumbersome and unelegant, but it is also expensive for all 10 listeners to do exactly the same thing. So what to do? What works? — Events bubbling up!

For each of the 10 Li’s, no matter which li the click occurs on, the click event will eventually bubble up to their common parent, the UL element, so it’s perfectly ok for ul to help sense the click event.

Since UL can help sense events, can it help handle them? The answer is yes, because of E.target. Ul elements can use the target attribute of the event object to get the element that actually triggers the event, and distribute the event processing logic to this element, thus achieving true “delegation”.

With this in mind, you can write code without the for loop. Here is code that uses the event broker to achieve the same effect:

var ul = document.getElementById('poem')
ul.addEventListener('click', function(e){
  console.log(e.target.innerHTML)
})
Copy the code

Again, the e.target attribute, which refers to the specific target that triggered the event, records the source of the event. So no matter what layer the listener executes on, if you get the value of e.target, you get the element that actually triggered the event. Once you’ve got this element, you can simulate its behavior to achieve an undifferentiated listening effect.

An action like this that takes advantage of the bubbling nature of events to merge the same type of listening logic from multiple child elements into a parent element managed by a listener function is called event delegate. Through event delegate, memory overhead can be reduced, registration steps can be simplified, and development efficiency can be greatly improved.

This wonderful event delegate is the inspiration for the React composite event.

3. How does the React event system work

React’s event system follows the idea of event delegation. In React, except for a few special non-bubbling events (such as media-type events) that cannot be handled by the event system, most events are not bound to specific elements. Instead, they are bound to the document of the page. When events are fired on specific DOM nodes, they eventually bubble up to the Document, which is bound to a unified event handler that distributes the event to specific component instances.

React first wraps events before distributing them, wrapping native DOM events as composite events.

1) Recognize React synthesis events

React is a custom event object that complies with the W3C specification. It flattens out the differences between browsers at the bottom and exposes a unified, stable event interface for developers at the top that is identical to DOM native events. Developers can now focus on business logic instead of onerous compatibility issues.

Although the synthesized event is not a native DOM event, it retains a reference to the native DOM event. When you need access to the native DOM event object, you can get it from the synthesized event object’s e.ativeEvent property, as shown below:

E.ativeevent will output MouseEvent, the native event, as shown below:

React event system the React event system is a complex event system. Next, the React source code and call stack will be combined on this basis, the workflow of the event system will be deeply disassembled.

React Event system workflow disassembly

Since it is an event system, it cannot escape from the two key actions of “event binding” and “event triggering”. Let’s start by looking at how event binding is implemented.

(1) Binding of events

The binding of events is done during the component’s mount, specifically, in the completeWork. Having learned how completeWork works in Lecture 16 (link here), here are three actions to remember in completeWork:

There are three key actions within completeWork: Create a DOM node (createInstance), insert the DOM node into the DOM tree (appendAllChildren), and set attributes for the DOM node (finalizeInitialChildren).

The “Setting properties for DOM Node” section traverses the Props key of FiberNode. When traversing the functions associated with the event, the registration link for the event is triggered. The function call stack involved in the whole process is shown below:

How do these functions fit together? Take a look at the workflow diagram below:

As you can see from the figure, the event registration process is started by the ensureListeningTo function. In ensureListeningTo, you try to get the root node in the current DOM structure (in this case, the Document object), and then register the unified event-listening function on the Document by calling legacyListenToEvent.

In legacyListenToEvent, is, in fact, by calling the legacyListenToTopLevelEvent to handle the relationship between the event and the document. LegacyListenToTopLevelEvent literal translation is “listening top event”, the “top” can be understood as the top event delegation, which is the document node. In legacyListenToTopLevelEvent, there is such a logic note, please see below:

ListenerMap is a data structure created/acquired in legacyListenToEvent that records which events the current Document has listened on. In legacyListenToTopLevelEvent logic starting point, will first determines listenerMap. From the (topLevelType) whether the condition is true.

Here’s a little bit of pre-knowledge: TopLevelType function in legacyListenToTopLevelEvent context represents the type of event, such as I try to listen is a click event, then topLevelType value will be the click, as shown in the figure below:

If the event system recognizes that ListenerMap. has(topLevelType) is true, that is, the current event document is already listening, then the event will be skipped directly, otherwise the specific event listening logic will be entered. As a result, even if a listener for the same event is called multiple times in the React project, only one registration will be triggered on the Document.

Why should Document register only one listener for the same event, even though there may be multiple callbacks? React registers the document not with specific callback logic on a DOM node, but with a unified event distribution function. Here the breakpoint is placed on the binding action of the event listener function, as shown below:

In this logic, Element is the DOCUMENT DOM element, as shown in the figure below. After it is acquired in the legacyListenToEvent stage, it is passed to this position by layers of logic.

Not to mention the addEventListener, which is an interface in the native DOM specifically used to register event listeners. The real focus here is on the first two input arguments to the function in the figure. First look at eventType, which represents the type of event. Here we are listening for a click event, so eventType is Click (see the run-time output below).

The listener, as mentioned earlier, is a unified event distribution function that is finally registered with the document. What does this function look like? Here is the listener output at runtime:

As you can see, the Listener body is a function called dispatchDiscreteEvent. In fact, depending on the situation, a listener can be one of three functions:

  • dispatchDiscreteEvent

  • dispatchUserBlockingUpdate

  • dispatchEvent

DispatchDiscreteEvent and dispatchUserBlockingUpdate, mainly reflected on the processing of the priority, affect event distribution action was fine. In both dispatchDiscreteEvent dispatchUserBlockingUpdate, finally all of them is by calling the dispatchEvent to perform event distribution. So you can assume that the unified event distribution function that you end up binding to document is actually dispatchEvent.

So how does dispatchEvent implement event distribution?

(2) Event triggering

The essence of event triggering is a call to the dispatchEvent function. Since the call link triggered by dispatchEvent is long and there are too many elements involved in the process, it is no longer necessary to trace the call stack of functions one by one, but to directly look at the core workflow, as shown below:

The first three steps in workflow have been mentioned above, but the more difficult ones are steps 4, 5, and 6, which are the focus of the rest of the presentation.

(3) Collection and execution of event callback

To understand this process, use a Demo component code as follows:

import React from 'react'; import { useState } from 'react' function App() { const [state, SetState] = useState(0) return (<div className="App"> <div id="container" onClickCapture={() => console.log(' Capture after Div ')} onClick={() => console.log(' bubble over div')} className="container"> <p style={{width: 128, textAlign: 'center' }}> {state} </p> <button style={{ width: 128}} onClick = {() = > {setState (state + 1)}} + 1 > click < / button > < / div > < / div >). } export default App;Copy the code

The interface for this component is shown below:

The interface renders a line of numeric text and a button that +1 for each click of the button. In the JSX structure, in addition to the button button, a DIV element with the id container listens for the click event to bubble and capture.

The Fiber tree structure corresponding to App components is shown in the figure below:

Next, use this Fiber tree structure to understand how event callbacks are collected.

First, look at the source logic for the collection process. This part of the logic is in the traverseTwoPhase function, the source code is as follows (parsing in the comments) :

Function traverseTwoPhase(inst, fn, arg) {var path = []; While (inst) {// Collect the current node into the path array path.push(inst); // Collect tag=== Parent of HostComponent inst = getParent(inst); } var i; For (I = path.length; i-- > 0;) { fn(path[i], 'captured', arg); } // From front to back, collect the nodes in the path array that will participate in the bubbling process and the corresponding callback for (I = 0; i < path.length; i++) { fn(path[i], 'bubbled', arg); }}Copy the code

The traverseTwoPhase function does three things.

(1).Loop through the collection of eligible parent nodes and store them in the PATH array

The traverseTwoPhase starts with the current node (the target node that triggered the event) and goes up to the parent node of the Tag ===HostComponent and collects these nodes in sequence into the PATH array. The tag===HostComponent condition is managed in getParent().

Why do tag===HostComponent have to be required? As mentioned earlier in the link rendering section, HostComponent is the Fiber node type corresponding to the DOM element. Tag ===HostComponent is limited, that is, only the Fiber nodes corresponding to DOM elements are collected. This is done because browsers only know DOM nodes, browser events are propagated only between DOM nodes, and collecting other nodes is pointless.

This process corresponds to the Fiber tree in the Demo. The button node is the starting point for the event, and the only parent nodes that meet the tag===HostComponent criteria are div#container and div.App (highlighted below).

So the final contents of the path array collected are div#container, div.App, and the button node itself (remember, the button node is the start of the while loop and is pushed into the PATH array at the beginning), as shown below:

(2).Simulate the propagation order of events in the capture phase and collect node instances and callback functions related to the capture phase

Next, the traverseTwoPhase iterates through the PATH array from back to front, simulating the capture order of events and collecting the corresponding callbacks and instances of events during the capture phase.

The path array is collected from the child nodes and up. So the path array is preceded by the neutron node and followed by the ancestor node.

Traversing the path array from back to front is the process of traversing the child nodes from the parent node down to the target node in the same order that events propagate during the capture phase. In the traversal process, fn function will check the callback of each node. If the capture callback corresponding to the current event on this node is not null, The node instance is then collected in the _dispatchInstances property of the SyntheticEvent, Event callbacks are collected on the _dispatchListeners property of synthetic events (syntheticEvent._dispatchListeners) for subsequent execution.

(3).Simulate the propagation order of events in the bubbling phase, collect the node instances and callback functions related to the bubbling phase

After the capture phase is complete, traverseTwoPhase iterates through the PATH array from back to front, simulating the bubbling order of events, and collecting the callbacks and instances that correspond to the events during the capture phase.

This process is basically the same as step 2, except that the backward traversal of the PATH array becomes the forward traversal. Since backward traversal simulates the event propagation order in the capture stage, positive traversal naturally simulates the event propagation order in the bubbling stage. In the process of sequential traversal, the callback of each node is also checked. If the bubble callback corresponding to the current event on this node is not empty, Instances of nodes and event call-ins are also collected by SyntheticEvent._dispatchinInstances and SyntheticEvent._dispatchListeners respectively.

参 数 SyntheticEvent 参 数 SyntheticEvent 参 数 SyntheticEvent 参 数 SyntheticEvent 参 数 SyntheticEvent Collected event callbacks are also pushed into the same SyntheticEvent._dispatchlisteners.

This allows the entire DOM event stream to be simulated in one sitting by executing the syntheticEvent._dispatchListeners array in sequence during the event callback phase, which is the catch-target-bubble phase.

The following are examples of the _dispatchInstances and _dispatchListeners on the SyntheticEvent object corresponding to the click event triggered by the Demo. Please see the following figure:

Note that the elements of the _dispatchInstances and _dispatchListeners array are in strict one-to-one correspondence, which ensures that during the callback process, instances can be easily linked to listener functions through indexes to achieve event delegate effect. At the same time, the order of elements in the two arrays perfectly conforms to the “capture – target – bubble” three stages of the DOM standard event propagation order, which is fantastic!

4, summarize

This tutorial reviews the native DOM event flow and introduces the workflow of the React event system. Here’s a question: Why don’t React just use the event mechanism provided by the native DOM?

Or to put it another way: What was the motivation for designing the React event system?

Here are two ways to think to inspire you:

First, and here’s what React officially says: Composite events comply with W3C specifications, smoothen browser differences at the bottom, and expose developers to a unified, stable event interface that is identical to DOM native events at the top. Developers can now focus on developing business logic without having to worry about the complexities of underlying compatibility.

In addition, React’s self-developed event system gives React a firm grip on event handling: it’s the same as building a wheel. Before looking into a front-end framework, it’s hard to avoid the question “Why do I need to do it myself? React doesn’t work well? Doesn’t Vue smell good?” . You know, we don’t build wheels because other people’s wheels aren’t good, but because other people’s wheels don’t fit well into the situation we’re in. For example, if React wants to handle fiber-related priority concepts in an event system, or if it wants to combine multiple events into a single event (such as onChange), it’s hard to do with native DOM. Because it’s all about universality. React, on the other hand, wants to be “tailored”. ** By developing its own event system, React can largely intervene in the performance of events to meet its own needs.

React composite events inherit the idea of event delegation, but its implementation process is much more complicated than traditional event delegation. For React, the main function of event delegation should be to help React realize the centralized control of all events. Do React events perform better than native DOM events without event delegates? It’s hard to draw conclusions without rigorous comparisons and a lot of test data, and React officials have never made a similar statement. For the sake of rigor, it is not recommended to use performance as a starting point for understanding the characteristics of composite events.

So that’s it for the React event system. Next up is Redux’s world.

5, the appendix

To study the source