preface

React virtual Dom and its diff algorithm are the main reasons why React rendering efficiency is much higher than traditional Dom rendering efficiency. On the one hand, the existence of the virtual Dom makes it no longer necessary to operate the page Dom directly when operating the Dom, but to carry out relevant operations on the virtual Dom. Then, based on the calculation results and diff algorithm, the changed part of Dom is obtained for local update. On the other hand, when there are very frequent operations, merge operations. Dom updates are made directly after the final state has been computed. This greatly improves Dom rendering efficiency. React uses the DIff algorithm to compare Dom changes. React has a complex internal operation process. This article does not discuss the specific code level. Just through a small Demo to explore the macro diff operation ideas.

Diff’s comparison

React diff comparison uses the rules of deep traversal for traversal comparison. Take the FOLLOWING Dom structure as an example:



The comparison process is as follows:Contrast Component 1 (no change) -> Contrast Component 2 (no change) -> Contrast Component 4 (no change) -> Contrast Component 5 (Component 5 is removed and a removal operation is recorded) -> Contrast Component 3 (no change) -> Contrast component 3 subcomponent (component 5 is added and a new operation is recorded).At the end of the comparison, the change data records the changes of the two nodes. At render time, a removal of component 5 and a new addition of component 5 are performed. Other nodes do not change, so as to realize the page Dom update operation.

The Demo que

Next, let’s design a simple demo to analyze the entire process as the page changes. First we create several identical Demo components:

    import React, { Component } from 'react';
    export default class Demo1 extends Component {
        componentWillMount() {
            console.log('Load component 1');
        }
        componentWillUnmount() {
            console.log('Destroy component 1')
        }
        render () {
            return <div>{this.props.children}</div>}}Copy the code

In addition to rendering its internal Dom directly, components also print logs in the console before loading and before unloading components. The component structure shown in the figure above is then combined with code and changes triggered by events.

/ / before the change<Demo1>1
        <Demo2>2
            <Demo4>4</Demo4>
            <Demo5>5</Demo5>
        </Demo2>
        <Demo3>3</Demo3>
    </Demo1>/ / after the change<Demo1>1
        <Demo2>2
            <Demo4>4</Demo4>
        </Demo2>
        <Demo3>3
            <Demo5>5</Demo5>
        </Demo3>
    </Demo1>
Copy the code

The console prints a log after a change operation is performed

Load component 5 and destroy component 5Copy the code

Result As in the analysis, component 5 is loaded and component 5 is uninstalled.

Let’s look at some of the complications.

Let’s start with the Dom deletion



According to the previous analysis, the comparison process is as follows:Contrast Component 1 (no change) -> Contrast Component 2 (no change) -> Contrast Component 4 (Component 4 is removed, a removal operation is recorded) -> Contrast Component 5 (no change) -> Contrast Component 6 (no change) -> Contrast Component 3 (no change). End of comparison. Following this analysis, after testing the code, the console log should output:

Destroy Component 4Copy the code

This log. However, after the actual test, you find that the output log is:

Load components 5 Load components 6 Destroy components 4 Destroy components 5 Destroy components 6Copy the code

You can see that in addition to the “Destroy component 4” single operation, components 5 and 6 are destroyed and loaded. Could it be that our previous analysis was wrong? Hang on, let’s do another experiment:

Similarly, only one component is deleted, but the location of the deleted component is different. According to the last experiment result, the console output log should be:

Load a component. 4 Load a component. 5 Destroy a componentCopy the code

However, the actual results of the experiment were beyond our expectation. The actual output is as follows:

Destroying Component 6Copy the code

This is an interesting phenomenon. Just by removing components in different locations, the diff analysis process is completely different. In fact, if you continue to experiment with removing component 5, you will find that the results are completely different from the previous two times.

In fact, the DIff algorithm cannot accurately compare changes to the virtual Dom one-to-one. (Of course, React provides a solution, which will be discussed later.) When a parent node changes, all of its children are destroyed. And its sibling nodes are compared in one-to-one order according to node order. So in the first example above, the comparison order actually looks like this:Compare Component 1 (no change) -> Compare Component 2 (no change) -> Compare Component 4 (component 4 is changed to component 5, record a removal operation of component 4 and a new operation of component 5) -> Compare Component 5 (Component 5 is changed to component 6, Record a removal of component 5 and a new addition of component 6) -> Compare component 6 (when component 6 is removed, record a removal of component 6). End of comparison. Following this line of analysis, the console output is not hard to understand.

Also when we remove component 6 in the second example. The order of components 4 and 5 has not changed, so the comparison is still made to the virtual Dom of the component itself, so there is only one removal of component 6.

We can further test the conjecture by adding and modifying operations.

By adding a component before component 4 versus adding a component after component 6. We can find that the result is exactly the same as our guess. The specific experimental deduction process will not be described here.

For modifications, only the addition of the replacement component and its children and the removal of the replaced component are performed because the modification does not change the number or order of the component and its siblings.

Peer component analysis is done, but what about component operations across hierarchies? For example, the following DOM change:



Since the structure of component 2, component 4 and component 5 has not changed at all, will the whole structure be reused and only the relative position be changed? The experiment found that the console log output was:

Load a component. 3 Load a component. 2 Load a component. 4 Load a component. 5 Destroy a componentCopy the code

When visible component 2 and its children change, component 2 and all children under it are rerendered. So why is component 3 also being rerendered? In fact, the reason is not that the child node is added, but that its sibling node 2 is removed, affecting its relative position. The complete comparison process is as follows: Compare component 1 (no change) -> Compare component 2 (component 2 is changed to component 3, record the removal operation of component 2 and its children: component 4 and component 5, and record the new operation of component 3 and its children: Component 2, component 4 and component 5 remove operations) -> compare component 3 (component 3 is removed, record the removal operation of component 3 analysis shows: when a node changes, all its children will be re-rendered. For example, in the previous example, components 4 and 5 remain unchanged by replacing component 2 with component 6 without structural changes, but since components 4 and 5 are children of component 2, changes to component 2 still cause components 4 and 4 to be re-rendered. In addition, when react updates the local Dom, it loads the new component first and then removes the component.

The key that was ignored

In our previous development work, we have certainly encountered the rendering of lists. React forces us to set a unique key for each entry in the list (otherwise the console will warn us), and officially forbids the use of subscripts in the list as keys. In React 16 and later, the new method of rendering multiple sibling nodes as arrays also requires us to add unique keys to each item. You may be wondering why this mandatory key, which seems to have no real effect, is a mandatory item.

Improved rendering efficiency

In fact, inReactWhen I do diff,keyThe value is critical because each key is the id of the virtual Dom node, which was not defined in our previous experimentkeyWhen comparing the virtual Dom, the diff operation does not know that the virtual Dom is the same as the previous one, so the sequential comparison scheme can only be adopted for one-to-one comparison. Hence the completely different output results from the previous analysis due to the different positions. And when we add for each componentkeyAfter value, due to a unique identifier, accurate comparison can be made during diff operation, no longer affected by position changes.

Back to the original delete experiment, add a unique key for each component:

/ / before the change<Demo1 key={1}>1
        <Demo2 key={2}>2
            <Demo4 key={4}>4</Demo4>
            <Demo5 key={5}>5</Demo5>
            <Demo6 key={6}>6</Demo6>
        </Demo2>
        <Demo3 key={3}>3</Demo3>
    </Demo1>/ / after the change<Demo1 key={1}>1
        <Demo2 key={2}>2
            <Demo4 key={4}>4</Demo4>
            <Demo5 key={5}>5</Demo5>
            <Demo6 key={6}>6</Demo6>
        </Demo2>
        <Demo3 key={3}>3</Demo3>
    </Demo1>
Copy the code

The run found that the output log was exactly what we originally thought it would be:

Destroy Component 4Copy the code

Rerendering of components 5 and 6 is avoided as opposed to operations without key values. Greatly improved rendering efficiency. At this point, it becomes obvious why list-like data must have a unique key value. Imagine an infinitely scrolling mobile list page loaded with 1,000 entries. The first item is deleted. Then, in the absence of a key value, to re-render the list, all 999 items after the first item need to be re-rendered. With a key, you only need to remove the first item once. It can be seen that the improvement of rendering efficiency by key value is absolutely huge.

Key cannot be set as data subscript

So why not set the key value to the subscript of the data? It’s actually very simple, because the subscript starts at zero, and again, the list on the mobile side, removes the first data, if you set the key to the data subscript. So the original data key value is 1, after to apply colours to a drawing, the key value will be set to 0, then, when compare to the data and change in front of the key data compare to 0, obviously, the two data is not the same, so will still because the data is different, lead to the entire list of rendering.

Must the key value be unique?

In addition, there is a common understanding in development that the key value must be unique. But can’t the key really be the same? According to the previous experiments and analysis, it can be seen that when comparing sibling nodes, the key value can be used as the only mark for accurate comparison. However, for non-sibling components, the diff operation adopts deep traversal, and the change of the parent component will completely update the child component. Therefore, theoretically, the key value has little effect on non-sibling components. It should be possible for non-sibling components to have the same key value. So let’s test our hypothesis experimentally.

/ / before the change<Demo1 key={1}>1
        <Demo2 key={1}>2
            <Demo4 key={4}>4</Demo4>
            <Demo5 key={5}>5</Demo5>
            <Demo6 key={6}>6</Demo6>
        </Demo2>
        <Demo3 key={2}>3
            <Demo4 key={4}>4</Demo4>
            <Demo5 key={5}>5</Demo5>
            <Demo6 key={6}>6</Demo6>
        </Demo3>
    </Demo1>/ / after the change<Demo1 key={1}>1
        <Demo2 key={1}>2
            <Demo5 key={5}>5</Demo5>
            <Demo6 key={6}>6</Demo6>
        </Demo2>
        <Demo3 key={2}>3
            <Demo4 key={4}>4</Demo4>
            <Demo6 key={6}>6</Demo6>
        </Demo3>
    </Demo1>
Copy the code

In this experiment, component 1 and component 2 have the same key value, and the children of component 2 and component 3 have the same key value, yet the code runs without warning about the same key value. After performing the Dom changes, the log output is as expected. Therefore, our conjecture is correct. The key value does not need to be absolutely unique, but only needs to be unique among siblings under the same parent node.

More uses of key

In addition to the above, once you understand how keys work, you can use key values to achieve a number of other effects. For example, a component that has a self-state can be updated with a key value. By changing the key value of the component, it can be rendered back to its original state. In addition, in addition to being used in lists, you can improve rendering efficiency by adding keys to any dom manipulation that affects the order of sibling nodes, such as adding or deleting them.