preface
The most effective way to learn is to share your knowledge with others.
Originally a very simple question, but I feel that I should remember it, it involves a lot of details can become a knowledge blind area.
Render multiple components
We double each item in the array using the map() function, and then we get a new list double and print it:
const numbers = [1, 2, 3, 4, 5]
const doubled = numbers.map((number) => number * 2)
console.log(doubled)
Copy the code
The code is printed [2, 4, 6, 8, 10]. In React, the process of converting an array to a list of elements is similar.
You can build a collection of elements within JSX by using {}.
Next, we use the map() method in Javascript to iterate over the numbers array. Change each element in the array to a
const numbers = [1, 2, 3, 4, 5]
const listItems = numbers.map((number) =>
<li>{number}</li>
)
Copy the code
We insert the entire listItems into the
-
element and render it into the DOM:
ReactDOM.render(
<ul>{listItems}</ul>,
document.getElementById('root')
)
Copy the code
This code generates a bulleted list from 1 to 5.
Base list component
Usually you need to render the list in a component.
We can reconstitute the previous example as a component that takes the numbers array as an argument and outputs a list of elements.
function NumberList(props) {
const numbers = props.numbers;
const listItems = numbers.map((number) =>
<li>{number}</li>
);
return (
<ul>{listItems}</ul>
);
}
const numbers = [1, 2, 3, 4, 5];
ReactDOM.render(
<NumberList numbers={numbers} />,
document.getElementById('root')
)
Copy the code
When we run this code, we’ll see a warning that a key should be provided for list items, which means that when you create an element, you must include a special key property.
Let’s assign a key attribute to each list element to address the warning above:
function NumberList(props) {
const numbers = props.numbers;
const listItems = numbers.map((number) =>
<li key={number.toString()}>
{number}
</li>
);
return (
<ul>{listItems}</ul>
);
}
const numbers = [1, 2, 3, 4, 5];
ReactDOM.render(
<NumberList numbers={numbers} />,
document.getElementById('root')
)
Copy the code
key
Key helps React identify which elements have changed, such as being added or removed. So you should give each element in the array a definite identity.
const numbers = [1, 2, 3, 4, 5];
const listItems = numbers.map((number) =>
<li key={number.toString()}>
{number}
</li>
)
Copy the code
An element’s key should ideally be a unique string that the element has in the list. In general, we use the id in the data as the element key:
const todoItems = todos.map((todo) =>
<li key={todo.id}>
{todo.text}
</li>
)
Copy the code
When an element has no specified ID, as a last resort you can use the element index index as key:
const todoItems = todos.map((todo, index) =>
// Only do this if items have no stable IDs
<li key={index}>
{todo.text}
</li>
)
Copy the code
If the order of list items may change, we do not recommend using an index as a key value, as this can lead to poor performance and possibly component state issues. Check out Robin Pokorny’s in-depth analysis of the negative effects of using indexes as keys. If you choose not to specify an explicit key, React will default to using the index as the key for list items.
If you’re interested in learning more, here’s an article that delves into why keys are necessary.
The lack ofkey
Common warnings are shown below:
Extract components with keys
The element’s key makes sense only if it is placed in the context of the nearest array.
For example, if you extract a ListItem component, you should keep the key on the
Example: Incorrect use of key
function ListItem(props) { const value = props.value; Return (// error! You don't need to specify key here: <li key={value.tostring ()}> {value} </li>); } function NumberList(props) { const numbers = props.numbers; Const listItems = numbers. Map ((number) => // error! The key of the element should be specified here: <ListItem value={number} />); return ( <ul> {listItems} </ul> ); } const numbers = [1, 2, 3, 4, 5]; ReactDOM.render( <NumberList numbers={numbers} />, document.getElementById('root') );Copy the code
Example: The correct way to use key
Function ListItem(props) {// Correct! Return <li>{props. Value}</li>; } function NumberList(props) { const numbers = props.numbers; Const listItems = numbers. Map ((number) => Key should be specified in the context of an array <ListItem key={number.tostring ()} value={number} />); return ( <ul> {listItems} </ul> ); } const numbers = [1, 2, 3, 4, 5]; ReactDOM.render( <NumberList numbers={numbers} />, document.getElementById('root') )Copy the code
A good rule of thumb is that elements in the map() method need a key attribute.
The key must only be unique between sibling nodes
A key used in an array element should be unique among its siblings. However, they need not be globally unique. When we generate two different arrays, we can use the same key value:
function Blog(props) {
const sidebar = (
<ul>
{props.posts.map((post) =>
<li key={post.id}>
{post.title}
</li>
)}
</ul>
);
const content = props.posts.map((post) =>
<div key={post.id}>
<h3>{post.title}</h3>
<p>{post.content}</p>
</div>
);
return (
<div>
{sidebar}
<hr />
{content}
</div>
);
}
const posts = [
{id: 1, title: 'Hello World', content: 'Welcome to learning React!'},
{id: 2, title: 'Installation', content: 'You can install React from npm.'}
];
ReactDOM.render(
<Blog posts={posts} />,
document.getElementById('root')
)
Copy the code
The key will pass information to React, but not to your components. If you need the value of the key attribute in your component, pass it explicitly with another attribute name:
const content = posts.map((post) =>
<Post
key={post.id}
id={post.id}
title={post.title} />
)
Copy the code
In the example above, the Post component can read props. Id, but not props. Key.
Embed map() in JSX
In the above example, we declared a single listItems variable and included it in JSX: listItems = listItems;
function NumberList(props) {
const numbers = props.numbers;
const listItems = numbers.map((number) =>
<ListItem key={number.toString()}
value={number} />
);
return (
<ul>
{listItems}
</ul>
);
}
Copy the code
JSX allows any expression to be embedded in braces, so we can inline the result returned by map() :
function NumberList(props) {
const numbers = props.numbers;
return (
<ul>
{numbers.map((number) =>
<ListItem key={number.toString()}
value={number} />
)}
</ul>
);
}
Copy the code
Sometimes this can make your code clearer, but sometimes this style can be abused. Just as in JavaScript, it’s entirely up to you when you need to extract a variable for readability. But keep in mind that if a map() has too many levels nested, that might be a good time to extract components.
Recurse on the child nodes
By default, React iterates through lists of both child elements while recursively iterating through the child elements of a DOM node. When a difference occurs, a mutation is generated.
When new elements are added at the end of the child element list, the update overhead is low. Such as:
<ul>
<li>first</li>
<li>second</li>
</ul>
<ul>
<li>first</li>
<li>second</li>
<li>third</li>
</ul>
Copy the code
React matches two
trees, then matches the second< li>second tree, and finally inserts the
tree of the third element.
If you simply insert the new element into the table header, the update will be expensive. Such as:
<ul>
<li>Duke</li>
<li>Villanova</li>
</ul>
<ul>
<li>Connecticut</li>
<li>Duke</li>
<li>Villanova</li>
</ul>
Copy the code
React will not realize that it should keep
and
, but will rebuild each child element. This situation can cause performance problems.
Keys
To address these issues, React supports the key attribute. When a child element has a key, React uses the key to match the child element of the original tree with the child element of the latest tree. The following example makes a previously inefficient conversion efficient after adding a key:
<ul>
<li key="2015">Duke</li>
<li key="2016">Villanova</li>
</ul>
<ul>
<li key="2014">Connecticut</li>
<li key="2015">Duke</li>
<li key="2016">Villanova</li>
</ul>
Copy the code
React now knows that only elements with the ‘2014’ key are new; elements with the ‘2015’ and ‘2016’ keys are simply moved.
In the real world, it is not difficult to generate a key. The element you want to present may already have a unique ID, so the key can be extracted directly from your data:
<li key={item.id}>{item.name}</li>
Copy the code
When this is not the case, you can add an ID field to your model, or generate a key using a portion of the content as a hash. This key doesn’t need to be globally unique, but it needs to be unique in the list.
Finally, you can also use the element’s index in the array as a key. This strategy is appropriate when elements are not being reordered, and diff will slow down if the order changes.
Component state may encounter some problems when subscription-based components are reordered. Because component instances decide whether to update and reuse based on their key, if the key is a subscript, the order in which changes are made changes the current key, causing states of uncontrolled components (such as input fields) to tamper with each other and change unpredictably.
Weigh the
Keep in mind that the coordination algorithm is an implementation detail. React can re-render the entire application after each action, and the end result will be the same. In this context, rerendering means calling the Render method within all components, not that React will unload or load them. React will only decide how to merge differences based on the rules mentioned above.
We regularly explore optimization algorithms to make common use cases perform more efficiently. In the current implementation, it is understood that a subtree can move between its siblings, but not anywhere else. In this case, the algorithm rerenders the entire subtree.
React relies on exploratory algorithms, so performance suffers when the following assumptions are not met.
- The algorithm does not attempt to match subtrees of different component types. If you find yourself switching between two different types of components with very similar output, it is recommended to change them to the same type. In practice, we have encountered no such problems.
Key
It should be stable, predictable, and unique to the list. unstablekey
(Such as throughMath.random()
Generated) results in many component instances andDOM
Nodes are recreated unnecessarily, which can lead to performance degradation and loss of state in child components.
Explain the DIff algorithm
In the book Deep into the React Technology Stack, it is written that diff, as the accelerator of the Virtual DOM, has improved and optimized its algorithm, which is the basis and performance guarantee of the entire React interface rendering. It is also the most mysterious and incredible part of the React source code.
React is best known for its combination of the Virtual DOM model and Diff, especially its efficient DIff algorithm, which allows users to refresh pages “freely” without worrying about performance issues and developers to not care about how Virtual DOM works. Because diff helps us figure out what really changes in the Virtual DOM and only do native DOM operations on that part of the Virtual DOM instead of re-rendering the entire page, it ensures an efficient rendering of the page after each update. Therefore, the Virtual DOM model and diff are the driving force behind React performance reputation. The diff algorithm was not invented. Just because the popularity of the algorithm is high, the efforts and contributions made by React to the optimization of diff algorithm should be recognized, which better reflects the charm and wisdom of React creators!
Traditional DIff algorithm
It is a complex and worthy problem to calculate the minimum operation of transforming one tree structure into another tree structure. The traditional DIFF algorithm compares nodes in turn through cyclic recursion, which is inefficient and the algorithm complexity reaches O(N3), where N is the total number of nodes in the tree. How scary is O(n3)? This means that if you want to show 1000 nodes, you have to perform a billion comparisons in turn. This exponential performance cost is too high for the front-end rendering scene. Today’s cpus can execute about 3 billion instructions per second, and even the most efficient implementations can’t calculate the difference in a second. Therefore, if React simply introduced the DIff algorithm without any optimization improvements, it would be far from efficient enough to meet the performance requirements of front-end rendering. If you want to introduce the idea of diff into the Virtual DOM, you need to design a stable and efficient diff algorithm. React does this! So how does diff work?
Rounding out the diff
React Converts the Virtual DOM tree into the actual DOM tree using a minimum of operations called reconciliation. Diff algorithm is a concrete implementation of harmonization. So how does this work?
React converts O(N3) complexity problems into O(n) complexity problems by making bold strategies.
O(n) is a completely linear, breadth-first hierarchical comparison, starting at the root node.
1. The diff strategy
The React Diff algorithm uses three strategies.
- Strategy one:
Web UI
中DOM
The movement of nodes across hierarchies is extremely rare and can be ignored. - Strategy 2: Two components with the same class will generate similar tree structures, and two components with different classes will generate different tree structures.
- Strategy three: For a group of children at the same level, they can pass unique
id
Make a distinction.
Based on the above strategies, React optimizes the tree Diff, Component Diff, and Element Diff algorithms. It turns out that these three prerequisite strategies are reasonable and accurate, which guarantee the performance of the overall interface construction.
2. tree diff
Based on strategy 1, React optimizes the algorithm of trees in a simple and straightforward way, that is, the trees are compared at different levels. Two trees will only compare nodes at the same level. React uses updateDepth to control the hierarchy of the Virtual DOM tree. Only DOM nodes at the same level are compared, that is, all child nodes under the same parent node. When a node is found to no longer exist, the node and its children are removed completely and are not used for further comparison. This allows you to compare the entire DOM tree with only one walk through the tree.
// updateChildren: updateChildren: function(nextNestedChildrenElements, transaction, context) { updateDepth++; var errorThrown = true; try { this._updateChildren(nextNestedChildrenElements, transaction, context); errorThrown = false; } finally { updateDepth--; if (! updateDepth) { if (errorThrown) { clearQueue(); } else { processQueue(); }}}}Copy the code
You might be wondering how diff would behave if DOM nodes were moved across hierarchies.
As shown in the figure below, node A (including its children) is moved to node D entirely. React only takes into account the position change of nodes at the same level, but only creates and deletes nodes at different levels. When the root node finds that A has disappeared in the child node, it destroys A directly. When D discovers that A child node A is added, it creates A new child node (including the child node) as its child node. Diff :create A → create B → create C → delete A
It can be seen that when A node is moved across the hierarchy, the imaginary movement operation does not occur, but the entire tree with node A as the root is recreated. This is an operation that affects React performance, so it is not recommended to operate across DOM nodes.
Note that when developing components, maintaining a stable DOM structure can help improve performance. For example, you can hide or show nodes through CSS without actually removing or adding DOM nodes.
3. component diff
React builds applications based on components and uses a concise and efficient strategy for comparing components.
- If the components are of the same type, continue the comparison according to the original policy
Virtual DOM
The tree. - If not, the component is judged to be
dirty component
To replace all child nodes under the entire component. - For components of the same type, it is possible that
Virtual DOM
Nothing has changed, and if you know that for sure, you can save a lot of moneydiff
Computation time. As a result,React
Allow users to passshouldComponentUpdate()
To determine whether the component needs to proceeddiff
Algorithm analysis.
As shown below, when componentsD
A component G
Even if the two components are structurally similar, once React
judge D
和 G
Is a different type of component, does not compare the structure of the two, but deletes the component directlyD
To recreate the componentG
And its sub-nodes. Although two components are of different types but similar in structure,diff
It affects performance, but just likeReact
The official blog says: There are few similarities between different types of componentsDOM
Tree, so it’s hard for this extreme factor to have a significant impact on the actual development process.
4. element diff
When nodes are at the same level, diff provides three node operations: INSERT_MARKUP, MOVE_EXISTING, and REMOVE_NODE.
INSERT_MARKUP
: The new component type is not in the old collection, that is, the new node needs to be inserted into the new node.MOVE_EXISTING
: There is a new component type in the old collection andelement
Is an updatable type,generateComponentChildren
Has been calledreceiveComponent
In this caseprevChild=nextChild
, you need to do mobile exercises
You can reuse the previous DOM node.
REMOVE_NODE
: old component type, also present in the new collection, but correspondingelement
If not, it cannot be reused and updated directly and needs to be deleted, or if the old component is not in the new collection, it also needs to be deleted.
As shown in the figure below, the old collection contains nodesA, B, C and D
, the updated new collection contains nodesB, A, D and C
At this point the old and new sets proceeddiff
Differentiation contrast, discoveryB ! = A
Is created and insertedB
To the new collection, delete the old collectionA
; And so on, create and insertA, D and C
, delete theB, C and D
.
React finds that such operations are cumbersome and redundant, because these nodes are the same, but their positions change, which requires complex and inefficient deletion and creation operations. In fact, you only need to move the positions of these nodes.
React proposes an optimization strategy that allows developers to add unique keys to subnodes of the same hierarchy. Even though this is a small change, it makes a huge difference in performance.
The nodes contained in the old and new sets are shown in the figure below. After diff differentiation comparison, it is found that the nodes in the new and old sets are all the same nodes through key. Therefore, node deletion and creation are not required, but the location of nodes in the old set is moved to the location of nodes in the new set. In this case, the React diff result is as follows :B and D do not perform any operations while A and C move.
So how does such an efficient diff work?
First, loop through for (name in nextChildren) the nodes in the new set, judge whether there is the same node in the new set if (prevChild === nextChild) by the unique key, if there is the same node, then move the operation. If (child._mountIndex < lastIndex), the position of the current node in the old set must be compared with that of lastIndex. Otherwise, this operation is not performed. This is a sequential optimization, and lastIndex is constantly updated to indicate the right-most (or largest) position of the visited node in the old set. If the currently accessed node in the new set is larger than lastIndex, it means that the currently accessed node is lower than the previous node in the old set, and the node does not affect the position of other nodes, so it is not added to the difference queue, that is, no move operation is performed. Only if the accessed node is smaller than lastIndex do you need to move it.
In the figure below, the diFF differentiation comparison process is more clearly and intuitively described.
- From the new collection
B
, and then determine whether the same node exists in the old setB
, a node is foundB
And then judge whether to move the node by comparing the node position.B
Position in the old setB._mountIndex = 1
At this time,lastIndex = 0
, does not meet thechild._mountIndex < lastIndex
Therefore, it is not trueB
Perform the move operation. updatelastIndex = Math.max(prevChild._mountIndex, lastIndex)
, includingprevChild._ mountIndex
saidB
Position in the old set, thenlastIndex = 1
And willB
Is updated to the location in the new collectionprevChild._mountIndex = nextIndex
, now in the new setB._mountIndex = 0
.nextIndex++
Enter the judgment of the next node.
- From the new collection
A
, and then determine whether the same node exists in the old setA
, a node is foundA
And then judge whether to move the node by comparing the node position.A
Position in the old setA._mountIndex = 0
When thelastIndex = 1
To meet thechild._mountIndex < lastIndex
The condition, therefore, ofA
Move operationenqueueMove(this, child._mountIndex, toIndex)
, includingtoIndex
In fact, isnextIndex
, the table belowA
Position to move to. updatelastIndex = Math.max(prevChild._mountIndex, lastIndex)
,lastIndex = 1
And willA
Is updated to the location in the new collectionprevChild._mountIndex = nextIndex
, now in the new setA._mountIndex = 1
.nextIndex++
Enter the judgment of the next node. - From the new collection
D
, and then determine whether the same node exists in the old setD
, a node is foundD
And then judge whether to move the node by comparing the node position.D
Position in the old setD._mountIndex = 3
, this
When lastIndex = 1, the condition of child._mountIndex < lastIndex is not met, so D is not moved. Prevchild._mountindex = math.max (prevchild._mountIndex, lastIndex) Prevchild. _mountIndex = nextIndex in the new set, then d._mountIndex = 2 in the new set 6, nextIndex++ enter the judgment of the next node.
- From the new collection
C
, and then determine whether the same node exists in the old setC
, a node is foundC
And then judge whether to move the node by comparing the node position.C
Position in the old setC._mountIndex = 2
At this time,lastIndex = 3
To meet thechild._mountIndex < lastIndex
The condition, therefore, ofC
Move operationenqueueMove(this, child._mountIndex, toIndex)
. updatelastIndex = Math.max(prevChild. 7 _mountIndex, lastIndex)
,lastIndex = 3
And willC
Is updated to the location in the new collectionprevChild._mountIndex = nextIndex
, now in the new setA._mountIndex = 3
.nextIndex++
Enter the judgment of the next node. Due to theC
It’s already the last node, sodiff
This completes the operation.
The above mainly analyzes the situation where there are the same nodes but different positions in the old and new sets, and the position of the nodes is moved. If the new collection has new nodes and the old collection has nodes that need to be deleted, how does the diff compare and contrast?
- From the new collection
B
, and then determine whether the same node exists in the old setB
, you can find that nodes existB
. Due to theB
Position in the old setB._mountIndex = 1
At this time,lastIndex = 0
Therefore, noB
Perform the move operation. updatelastIndex = 1
And willB
Is updated to the location in the new collectionB._mountIndex = 0
.nextIndex++
Enter the judgment of the next node. - From the new collection
E
, and then determine whether the same node exists in the old setE
, you can find that the node does not exist and create a new nodeE
. updatelastIndex = 1
And willE
Is updated to the position in the new collection,nextIndex++
Enter the judgment of the next node. - From the new collection
C
, and then determine whether the same node exists in the old setC
, you can find that nodes existC
. Due to theC
Position in the old setC._mountIndex = 2
.lastIndex = 1
At this time,C._mountIndex > lastIndex
Therefore, noC
Perform the move operation. updatelastIndex = 2
And willC
Is updated to the position in the new aggregation,nextIndex++
Enter the judgment of the next node. - From the new collection
A
, and then determine whether the same node exists in the old setA
, a node is foundA
. Due to theA
Position in the old setA._mountIndex = 0
.lastIndex = 2
At this time,A._mountIndex < lastIndex
And so onA
Perform the move operation. updatelastIndex = 2
And willA
Is updated to the position in the new collection,nextIndex++
Enter the judgment of the next node. - After the differential comparison of all nodes in the new set is completed, it is also necessary to cycle through the old set to determine whether there are nodes that are not in the new set but still exist in the old set. Such nodes are found at this time
D
Therefore, the node is deletedD
To thisdiff
The operation is complete.
Create, move, and delete nodes
Of course, diff still has some shortcomings and needs to be optimized. As shown in the figure below, if the nodes of the new set are updated as D, A, B and C, only D node moves compared with the old set, while A, B and C still maintain the original order. Theoretically, DIff should only move D. However, since D is the largest in the old set, The _mountIndex of another node is less than lastIndex. As A result, A, B, and C move to the rear of node D instead of node D.
The node of the new collection is updated toA, B, C, D
During development, minimize operations like moving the last node to the head of the list. When the number of nodes is too large or update operations are too frequent, React rendering performance will be affected to some extent.
conclusion
diff
The complexity of the algorithm isO(n)
Is a completely linear algorithm, breadth-first hierarchical comparison, starting at the root node.- You can use indexes
index
As akey
Value, you can do this if it does not involve peer node movement, deletion, or modification, but it is not recommended because it may cause component state problems. diff
The policy involvesTree DIff, Component Diff, Element Diff
That we usekey
iselement diff
Related content.