The code has been linked to github: link address if you feel good, you can click on the star, here will continue to share their experience in development (:
Virtual DOM
What is the virtual DOM
A programming concept that corresponds to a real DOM node, an object that represents the real DOM.
React virtual DOM
React belongs to ReactElement and has the following format:
const reactElement = {
key:null.props: {className:"".onClick: () = > {},
children:[]
},
type:'div'
}
Copy the code
Code creation: use React. CreateElement or JSX
React.createElement('div', {className:"".onClick: () = >{}},//child...
])
Copy the code
DOM slow? Virtual DOM fast?
The two are not simple generalizations, because the virtual DOM ultimately manipulates the real DOM.
- Real DOM operations are slower than native JS apis, such as group operations.
- Any DOM-based library (React, Vue), which manipulates the DOM, is no faster than the real DOM.
- When there are fewer normal nodes, the virtual DOM will render faster than the real DOM (not faster to operate, but shorter to not interact with), while when there are too many nodes, such as 10W, the real DOM will operate faster.
So why do people say DOM is slow and virtual DOM is fast? Because in some cases, the virtual DOM is actually faster than the real DOM. First, we need to understand the concept that JS computation is faster than dom manipulation
- Reduce DOM manipulation
- Combine DOM operations to reduce DOM operations. (Add 1000 nodes, calculate 1000 times, insert once)
- Using DOM diff, the virtual DOM can reduce the scope of DOM operations by eliminating unnecessary operations (add 1000 nodes, only 10 are new after comparison).
- cross-platform
The virtual DOM is essentially a JS object, so it can be converted into components like applets, Android views, and so on.
React and JS control the difference between native DOM
Events (such as Click)
- JS click events listen on the corresponding DOM, of course, can also be directly delegated to the parent element through the event delegate,
- React click events are listened on the virtual DOM, while React events are synthesized events, which can be understood as events delegated to the root node
Attributes (such as class style)
- JS directly to modify the corresponding attribute
- React triggers rerender, domDiff, merge and modify properties by changing props or state.
DOM diff
What is DOM Diff
The process of generating a new Fiber node by comparing the currently updated component to the corresponding Fiber node in the last rendering (commonly known as the Diff algorithm).
DOM Diff strategy
- Diff only sibling elements. React does not attempt to reuse a DOM node if it crosses the hierarchy in two updates.
- Two components with the same class will generate similar tree structures, and two components with different classes will generate different tree structures, so whenever the type of the current node changes, React destroys its descendant nodes and creates new descendant nodes.
- For the same level of array nodes, each child node can pass unique
key
Make a distinction.
DOM Diff processes are fully parsed
The new Diff entry function for reconcileChildFibers, which is part of the React Fiber task cycle, runs during the Fiber tree building phase.
This function compares the old Fiber node with the new ReactElement object and returns the new fiber node:
The React package location: packages/React – the reconciler/SRC/ReactChildFiber. Js
function reconcileChildFibers(
returnFiber: Fiber,
currentFirstChild: Fiber | null,
newChild: any,
lanes: Lanes,
) :Fiber | null {
const isObject = typeof newChild === 'object'&& newChild ! = =null;
if (isObject) {
// The object type is handled according to the element type
switch (newChild.$$typeof) {
case REACT_ELEMENT_TYPE:
// Calls the reconcileSingleElement to process the reconcileSingleElement
/ /... omit}}if (typeof newChild === 'string' || typeof newChild === 'number') {
The reconcileSingleTextNode is called
/ /... omit
}
if (isArray(newChild)) {
The reconcileChildrenArray is called to process the reconcileChildrenArray
/ /... omit
}
// Some other cases call handlers
/ /... omit
// If none of the above matches, delete the node
return deleteRemainingChildren(returnFiber, currentFirstChild);
}
Copy the code
As can be seen from the source code, DOM Diff will use different comparison algorithms according to the node type, of course, mainly in the case of single node and multi-node, we will analyze the two diff algorithms respectively below:
Single node Diff
Taking the case of whether to reuse a React element in the Object reconcileSingleElement for example, the Diff reconcileSingleElement comes into the reconcileSingleElement, and the overall logic is:
- According to the
key
And element typestype
Are consistent- If they are all the same, real DOM nodes can be reused (
stateNode
), - If inconsistent, mark that the fiber node needs to be removed and return the newly generated fiber node
- If they are all the same, real DOM nodes can be reused (
This function compares the old Fiber node with the new ReactElement object, and returns the new fiber node.
function reconcileSingleElement(
returnFiber: Fiber,
currentFirstChild: Fiber | null,
element: ReactElement,
lanes: Lanes,
) :Fiber {
const key = element.key;
let child = currentFirstChild;
// Check whether the current node exists
while(child ! = =null) {
// TODO: If key === null and child.key === null, then this only applies to
// the first item in the list.
// The key is the same
if (child.key === key) {
switch (child.tag) {
/ /... Omit case, the core is to determine whether the tag is the same, the same type indicates that it can be reused
if (child.elementType === element.type) {
// All other sibling elements are marked for deletion
deleteRemainingChildren(returnFiber, child.sibling);
// Clone the current fiber with the new properties and return
const existing = useFiber(child, element.props);
returnexisting; }}// The key is the same, but the tag is different. Subsequent sibling nodes do not need to be traversed, and both themselves and their sibling nodes are marked as deleted
deleteRemainingChildren(returnFiber, child);
break;
} else {
// If the key is different, the current node cannot be reused and the flag is deleted
deleteChild(returnFiber, child);
}
// Traverses all sibling nodes of the same level
child = child.sibling;
}
// omit creating a new Fiber and return
}
Copy the code
As you can see, the single-node Diff code is relatively simple. The only tricky point is that sibling nodes are marked as deleted in two cases:
- Reuse node, mark other sibling nodes to delete
key
Same thing, buttype
The difference is that the tag itself and other sibling nodes are removed
This is because the old Fiber tree renders multiple nodes, while the new one only has a single node. For example, there were three nodes before, but only one node is updated. When one node is reused, the other two nodes will be marked as deleted. Similarly, if a node with the same key cannot be reused, then of course all nodes should be marked as deleted.
Multi-node Diff
React is a bit more complicated than single-node Diff. Based on the higher frequency of DOM updates in the daily interface, React improves the priority of judging updates and divides Diff into two traversal stages:
- The first round is traversed to determine whether the node can be reused
- If reusable, continue traversing
- If it’s not reusable,
key
Differences lead to directThe end of the traverse;type
The current fiber node needs to be deleted to continue traversal - Until the new React element array
newChildren
Walk through, or the old Fiber nodeoldFiber
The traversal is complete.
- The second iteration will be handled according to the different circumstances of the first iteration
newChildren
和oldFiber
If the traversal is complete, only the update is performed.newChildren
After traversal,oldFiber
If the traversal is not complete, it indicates that the node needs to be deleted here, so the remaining nodes need to be traversedoldFiber
Node, marked for deletion.newChildren
I’m not done,oldFiber
If the traversal is complete, a new node needs to be inserted, and the old nodes have been reused, so the remaining nodes need to be traversednewChildren
Generate a new Fiber node.newChildren
andoldFiber
None of them have been traversed, indicating that some nodes have changed their positions and need to be moved in this update, which is also difficult to understand the Diff. We will analyze the Diff with detailed examples in the future.
For a multi-node Diff reconcileChildrenArray, it compares the old Fiber node with the new ReactElement array, and returns the first Fiber node in the array.
The React package location: packages/React – the reconciler/SRC/ReactChildFiber. Js
function reconcileChildrenArray(
returnFiber: Fiber,
currentFirstChild: Fiber | null,
newChildren: Array<*>,
lanes: Lanes,
) :Fiber | null {
let resultingFirstChild: Fiber | null = null;
let previousNewFiber: Fiber | null = null;
let oldFiber = currentFirstChild;
let lastPlacedIndex = 0;
let newIdx = 0;
let nextOldFiber = null;
//== The first iteration, newChildren
for(; oldFiber ! = =null && newIdx < newChildren.length; newIdx++) {
// The pointer of the old fiber is moved backwards to ensure the element index is consistent between traversals
if (oldFiber.index > newIdx) {
nextOldFiber = oldFiber;
oldFiber = null;
} else {
nextOldFiber = oldFiber.sibling;
}
// Compare index new and old, return null if the key is different
UseFiber (update logic) if the types are the same; createXXX(insert logic) if the types are different
const newFiber = updateSlot(
returnFiber,
oldFiber,
newChildren[newIdx],
lanes,
);
if (newFiber === null) {
// If null is returned, the key is different and the node is not updated
if (oldFiber === null) {
oldFiber = nextOldFiber;
}
break;
}
// This is not the first time to create fiber, shouldTrackSideEffects is set to true
if (shouldTrackSideEffects) {
// A new node is created. The old node needs to be marked as deleted
if (oldFiber && newFiber.alternate === null) { deleteChild(returnFiber, oldFiber); }}// lastPlacedIndex Index of the position of the last reusable node in oldFiber
// If the current node is reusable, determine whether the location is moved.
lastPlacedIndex = placeChild(newFiber, lastPlacedIndex, newIdx);
// Create a new fiber list
if (previousNewFiber === null) {
resultingFirstChild = newFiber;
} else {
previousNewFiber.sibling = newFiber;
}
previousNewFiber = newFiber;
oldFiber = nextOldFiber;
}
// oldFiber nodes need to be deleted, so the remaining oldFiber nodes need to be traversed, marked delete.
if (newIdx === newChildren.length) {
deleteRemainingChildren(returnFiber, oldFiber);
return resultingFirstChild;
}
// oldFiber traversal is complete, newChildren traversal is not complete, indicating that new nodes need to be inserted, and old nodes have been reused, so we need to traversal the remaining newChildren to generate new fiber nodes.
if (oldFiber === null) {
/ /... Omit this part of the code
// Notice that as in the first pass, if the current node is reusable, there is also a judgment whether the position is moved.
return resultingFirstChild;
}
//== Loop 2, loop newChildren
// Convert the old fiber to map, which is easy to find using key
const existingChildren = mapRemainingChildren(returnFiber, oldFiber);
// Walk through the rest of newChildren to find reusable nodes
for (; newIdx < newChildren.length; newIdx++) {
// Check whether the old node is reusable by key
const newFiber = updateFromMap(
existingChildren,
returnFiber,
newIdx,
newChildren[newIdx],
lanes,
);
if(newFiber ! = =null) {
if (shouldTrackSideEffects) {
if(newFiber.alternate ! = =null) {
existingChildren.delete(
newFiber.key === null? newIdx : newFiber.key, ); }}// As in the first pass, if the current node is reusable, then determine whether the position is moved.
lastPlacedIndex = placeChild(newFiber, lastPlacedIndex, newIdx);
if (previousNewFiber === null) {
resultingFirstChild = newFiber;
} else{ previousNewFiber.sibling = newFiber; } previousNewFiber = newFiber; }}if (shouldTrackSideEffects) {
// newChildren has been traversed, so the remaining nodes in oldFiber sequence are deemed to be deleted (mark Deletion).
existingChildren.forEach(child= > deleteChild(returnFiber, child));
}
return resultingFirstChild;
}
Copy the code
Resolution of node movement
Our goal is to find the moving node, so we need to clarify: does the node move what is the reference? React uses the lastPlacedIndex index for the position of the last reusable node in oldFiber.
Because updates are traversed according to newChildren, during traversal, if there is no movement, oldIndex of the current React element’s reusable node is always larger than lastPlacedIndex of the last reusable node. So if the current React element uses a smaller index, oldIndex, than lastPlacedIndex, it means that the updated old node has been moved to the right.
Here we follow the source code analysis:
function placeChild(
newFiber: Fiber,
lastPlacedIndex: number,
newIndex: number.) :number {
// New fiber node updated index
newFiber.index = newIndex;
if(! shouldTrackSideEffects) {// Noop.
return lastPlacedIndex;
}
// Get the reusable node fiber
const current = newFiber.alternate;
if(current ! = =null) {
const oldIndex = current.index;
// The previous position index of the reusable node is less than the position index to be inserted for this update, indicating that the node needs to be moved (to the right)
if (oldIndex < lastPlacedIndex) {
// This is a move.
newFiber.flags = Placement;
return lastPlacedIndex;
} else {
// This item can stay in place.
// The previous location index of the reusable node is greater than or equal to the location index to be inserted in this update, indicating that the node does not need to be moved
// If oldIndex >= lastPlacedIndex, lastPlacedIndex = oldIndex
returnoldIndex; }}else {
// This is an insertion.
// There are no reusable nodes
newFiber.flags = Placement;
returnlastPlacedIndex; }}Copy the code
Simple text and source code may still be difficult to understand, let’s take an example step by step analysis:
/ / before update
abcd
/ / updatedAbdc == First iteration == newChildren === ABDC oldFiber === ABCD lastPlacedIndex ===0A (before) vs a(after) is reusable oldIndex =0Is equal to lastPlacedIndex, don't move it, update lastPlacedIndex = oldIndex =0B (before) vs b(after) is reusable oldIndex =1, greater than lastPlacedIndex, no need to move, update lastPlacedIndex = oldIndex =1C (before) vs D (after) key changes, so it's not reusable, so I'm done iterating, and lastPlacedIndex =1
/ /.. The middle is omitted because deletion and insertion are not performed== newChildren == dc oldFiber === CD lastPlacedIndex ===1Comparison d oldFiber exists, reusable oldIndex =3Greater than lastPlacedIndex, you don't have to move it, lastPlacedIndex = oldIndex =3Compare c oldFiber existing, reusable oldIndex =2Is less than the last place index, so I need to move it, the last place index stays the same, or3NewChildren traversal completed == Finally == ABD three nodes do not move, c node moved to the right after DCopy the code
Abcd => ABDC. React actually locates the order of ABD first, and then c moves to the left. This also reminds us to avoid moving nodes forward. React will keep nodes in the same position and move nodes in front of it to the right, which costs performance. For example, abcd=>dabc. It would look like D would move first, but React would position D and ABC would move backwards.
If you are still confused or want to debug the source code for further understanding, you can go to github.com/wqhui/react… Download debugging.
The DOM example Diff
The Key role
-
Two child elements, delete the latter (if there is no key).
The tag type and tag attributes remain unchanged and do not need to be updated. The child element is changed from [1,2] to [2], the tag of 1 remains unchanged, but the children element is updated (the content of child element 2 is put here); The child element 2 is missing. Delete the corresponding DOM.
-
Two child elements, delete the latter (in the case of a key).
The tag type and tag attributes remain unchanged and do not need to be updated. The child element changes from [1,2] to [2], but because there is a key, the computer knows that the element key:1 has been deleted, and 2 remains unchanged, so it will directly delete 1 and keep 2.
reference
The React algorithm of harmonic react.iamkasong.com/diff/multi….