How does reactdom.render concatenate render links? (c)
In the previous chapter, we learned and mastered the call link of reactdom.render, including its corresponding initialization phase. Next, on this basis, learn the subsequent Render phase and commit phase. Among them, the render stage can be considered as the most core link in the whole render link, and the process of “finding different” that we repeatedly emphasize happens at this stage.
There are many things to do in the Render stage. Next, we will focus on the construction process of Fiber tree with beginWork as a clue.
1. Disassemble ReactDOM. Render call stack — Render phase
First, let’s review the location of the Render phase in the entire render link, as shown below.
In the figure, performSyncWorkOnRoot marks the start of the Render phase and finishSyncRender marks the end of the Render phase. This includes a lot of beginWork, completeWork call stacks, which is what Render does.
The beginWork and completeWork methods are important to note that they are connected in a “simulated recursion” process.
As emphasized in “Stack Reconciliation”, the reconciliation process under React 15 is a recursive process. Although the reconciliation process in Fiber architecture is not achieved by recursion, it is still a depth-first search process in the synchronous mode triggered by reactdom.render. In this process, the beginWork creates a new Fiber node, and the completeWork is responsible for mapping the Fiber node to the DOM node.
So here’s the problem: Until the last chapter, Fiber trees looked like this:
So how to traverse the fiber tree as shown in the figure above, and what is the final traversal result? Then go deep into the source code to find out!
1) Creation of the workInProgress node
As mentioned earlier, performSyncWorkOnRoot is the starting point of the Render phase, and the key thing about this function is that it calls renderRootSync. Let’s zoom in on the Performance call stack to see what happens immediately after renderRootSync is called:
This is followed by prepareFreshStack, which resets a new stack environment, and the most important step is the call to createWorkInProgress. The main logic for createWorkInProgress is extracted as follows:
Function createWorkInProgress(current, rootFiber) pendingProps) { var workInProgress = current.alternate; If (workInProgress === null) {// This is the first point to pay attention to, WorkInProgress = createFiber(current.tag, pendingProps, current.key, current.mode); workInProgress.elementType = current.elementType; workInProgress.type = current.type; workInProgress.stateNode = current.stateNode; // Alternate for workInProgress will point to current workInProgress. Alternate = current; Alternate = workInProgress; alternate = workInProgress; alternate = workInProgress; // return workInProgress; // Return workInProgress; }Copy the code
The first thing to declare is that the current input parameter in this function refers to the rootFiber object in the existing tree structure, as shown in the figure below:
Source code is too long (in fact, after processing is not long), its key points are as follows:
-
CreateWorkInProgress calls createFiber. WorkInProgress is the return value of the createFiber method.
-
WorkInProgress alternate will point to current;
-
The alternate for current will in turn point to workInProgress.
With these three points in mind, it’s natural to wonder what the workInProgress ontology looks like, and what createFiber returns. Here’s the createFiber logic:
var createFiber = function (tag, pendingProps, key, mode) {
return new FiberNode(tag, pendingProps, key, mode);
};
Copy the code
The code is surprisingly simple, but the information is pretty good — createFiber will create a FiberNode instance, which, as described in the previous section, is exactly the type of Fiber node. So workInProgress is a Fiber node. Not only that, but if you’re careful, you might notice that the workInProgress creation parameter actually comes from Current, as shown in the following code:
workInProgress = createFiber(current.tag, pendingProps, current.key, current.mode);
Copy the code
As you can see from the code, the workInProgress node is actually a copy of the Current node (rootFiber).
Combined with the information that current points to the rootFiber object (also a FiberNode instance) and that current and workInProgress are interconnected through alternate, it can be analyzed that after the execution of this operation, The structure of the entire tree should look like the following:
Once this is done, you enter the logic of workLoopSync. The workLoopSync function is also a simple workLoopSync function, and its logic is straightforward as follows:
Function workLoopSync() {// If workInProgress is null while (workInProgress! // Execute performUnitOfWork(workInProgress) against it; }}Copy the code
All workLoopSync does is iterate through the while loop to determine whether workInProgress is empty and execute the performUnitOfWork function on it if it isn’t.
The performUnitOfWork function triggers a call to beginWork, which in turn creates a new Fiber node. If the Fiber node created by beginWork is not empty, formUniofWork uses the new Fiber node to update the value of workInProgress in preparation for the next loop.
The beginWork is triggered by a loop call to performUnitOfWork, and new Fiber nodes are constantly created. When workInProgress is finally empty, there are no new nodes to create and the entire Fiber tree has been built.
In this process, each new Fiber node created is mounted, one by one, as a descendant of the original workInProgress node (highlighted below). The Fiber tree is also known as the workInProgress tree.
Accordingly, the tree in the figure to which the current pointer points to the root node is called the current tree.
A current tree and a workInProgress tree are, at least for now, identical (both have only one root node, after all). What is the purpose of React? Or what is it, after all, that one tree can’t do, and ultimately requires two “identical” trees to do it?
After a step-by-step understanding of how Fiber trees are built and updated, let’s look at the motivation behind the “two Fiber trees” phenomenon.
Let’s dive into the beginWork and completeWork logic and take a look at the construction process and final form of the Fiber tree.
2) beginWork Starts the Fiber node creation process
To be honest, beginWork’s source code is really too long. Therefore, in line with the principle of grasping the main contradiction, logical extraction is carried out for the actions strongly related to the tree construction process. The code is as follows (explained in the comments) :
function beginWork(current, workInProgress, renderLanes) { ...... // if (current! = null) if (current! = null) MemoizedProps; // memoizedProps = memoizedProps; var newProps = workInProgress.pendingProps; // If (oldProps! == newProps || hasContextChanged() || ( workInProgress.type ! == current. Type)) {didReceiveUpdate = true; } else if (XXX) {return A} else {if (didReceiveUpdate = true; } else {// Other cases where no updates are required, here our first render will execute logic to this line didReceiveUpdate = false; } } } else { didReceiveUpdate = false; }... Switch (workinprogress.tag) {...... // The root node will enter the logic case HostRoot: Return updateHostRoot(current, workInProgress, renderLanes) Return updateHostComponent(current, workInProgress, renderLanes) // The text node enters the logical case HostText: return updateHostText(current, workInProgress) ...... // omit a lot of things like "case: } // here is the error in the bottom, {{throw Error("Unknown unit of work tag (" + workinprogress.tag + "). This Error is likely caused by a bug in React. Please file an issue." ) } } }Copy the code
BeginWork source code is too long, here is the key summary:
-
The entry parameter of beginWork is a pair of workInProgress and current nodes connected with alternate;
-
The core logic of beginWork is to call different node creation functions according to the different tag attributes of fiber nodes (workInProgress).
-
The current node is rootFiber, and workInProgress is a copy of Current, both of which have a tag of 3, as shown below:
3 is the value of HostRoot, so the first beginWork will enter the updateHostRoot logic.
Don’t worry about the logical details of updateHostRoot for now. In fact, the entire switch logic contains functions such as “update+ type name”. In the example Demo, there are calls to updateHostRoot, updateHostComponent, etc. There are a dozen types of updateXXX. It is impossible to subtract logic from each function one by one.
Fortunately, not only do these functions have the same naming form, but they also work similarly. In the case of the Render links, the reconcileChildren method is called to generate child nodes of the current node.
The reconci Child’s source code is as follows:
function reconcileChildren(current, workInProgress, nextChildren, RenderLanes) {if (current === null) {if (current === null) { Workinprogress. child = mountChildFibers(workInProgress, null, nextChildren, renderLanes); } else {// if current is not null, For reconcileChildFibers. Child = reconcileChildFibers(workInProgress, Current. Child, nextChildren, renderLanes); }}Copy the code
From the reconcileChildren source, the reconcileChildren is just a logical distribution, with the work to be done in mountChildFibers and reconcileChildFibers.
3) ChildReconciler, the behind-the-scenes operator of Fiber nodes
And where are the mountChildFibers and reconcileChildFibers functions? In the source code, you can find two assignment statements like this:
var reconcileChildFibers = ChildReconciler(true);
var mountChildFibers = ChildReconciler(false);
Copy the code
It turns out that reconcileChildFibers and mountChildFibers not only have similar names, but also come from the same place. They are both the return values of ChildReconciler, with only the input differences. And ChildReconciler, is a solid “behemoth”, its internal logical quantity is comparable to N beginWork. Here the key elements are extracted as follows (explained in the comments) :
Function ChildReconciler(shouldTrackSideEffects) {function deleteChild(returnFiber, childToDelete) {if (! shouldTrackSideEffects) { // Noop. return; } // Perform the delete logic below}...... Function placeSingleChild(newFiber) {if (shouldTrackSideEffects && newFiber. Alternate === null) {if (shouldTrackSideEffects && newFiber. newFiber.flags = Placement; } return newFiber; Function placeChild(newFiber, lastPlacedIndex, newIndex) {newFiber. Index = newIndex; if (! shouldTrackSideEffects) { // Noop. return lastPlacedIndex; } // Perform the insert logic below}...... // Omit a series of updateXXX functions here, They are used for reconcileChildrenArray(returnFiber, currentFirstChild, newChildren, lanes) { ...... } // Omit a bunch of functions in the form of reconcileXXXXX, They deal with specific reconcileChildFibers function reconcileChildFibers(returnFiber, currentFirstChild, newChild, lanes) { After it reads the reconcileChildFibers, it goes through a series of conditions and calls the functions defined above for each node that operate on the reconcileChildFibers. }Copy the code
Since the original code is quite large, you can click on this file to see the details if you are interested. Here are just some key points to summarize for logic that is strongly related to the main flow:
-
(1) The key inclusion is shouldTrackSideEffects, which means “whether side effects need to be tracked,” so the difference between reconcileChildFibers and mountChildFibers lies in the treatment of those side effects;
-
(2) The ChildReconciler includes a number of functions such as placeXXX, deleteXXX, updateXXX, and reconcileXXX, which cover the creation, addition, deletion, and modification of Fiber nodes. Will be called directly or indirectly by reconcileChildFibers;
-
(3) The return value of ChildReconciler is a function called reconcileChildFibers, which is a logical distributor that performs different Fiber node operations, depending on the reconcileChildFibers, and ultimately returns different target Fiber nodes.
For point 1, here’s how to expand. How are side effects handled differently? PlaceSingleChild for example, the following is the placeSingleChild source code:
function placeSingleChild(newFiber) {
if (shouldTrackSideEffects && newFiber.alternate === null) {
newFiber.flags = Placement;
}
return newFiber;
}
Copy the code
As you can see, once we say shouldTrackSideEffects is false, then all of the following logic is not executed and returns directly. So what happens if it goes ahead? In short, put a flag called “Flags” on the Fiber node like this:
newFiber.flags = Placement;
Copy the code
So what does this flag called Flags do?
Since v17.0.0 is used here, the attribute name has been changed to flags, but in earlier versions it was called effectTag. EffectTag is more common and semantic in today’s community discussion, so we’ll use effectTag instead of flags.
Placement is an effectTag that tells the renderer, at the time of the actual DOM rendering, that I need a new DOM node here. Effecttags record the types of side effects. React defines side effects as actions such as fetching data, subscribing, or modifying the DOM. In this case, Placement clearly corresponds to DOM related side effects.
There are many other side effects, such as Placement, in the form of binary constants, partially captured below (see the effectTag type in this file) :
Back in the call link, since current is rootFiber, it is not null, so it will walk into the line of logic highlighted below. That is to say that between mountChildFibers and reconcileChildFibers, it chooses for each reconcileChildFibers:
In conjunction with the previous analysis, it is clear that the reconcileChildFibers are the return value of ChildReconciler(true). The input parameter is true, which means that its internal logic allows tracking side effects, so the “hit effectTag” action will take effect.
Next comes the logic of reconcileChildFibers, which, in the reconcileChildFibers’ logic dispensator, The creation of the rootFiber subnodes for reconcileXXX is distributed to a member of the reconcileXXX family of functions, in the form highlighted below:
Each FiberNode for the reconcileSingleElement is created based on the ReactElement object information of the rootFiber sub-nodes. The function calls involved in this process are highlighted below:
One thing to note here is that as the root node of the Fiber tree, rootFiber does not have an exact ReactElement mapping to it. In conjunction with the JSX structure, it can be understood as the parent node of the root component in JSX. In the Demo presented in this section, the component code is as follows:
import React from "react"; import ReactDOM from "react-dom"; Function App() {return (<div className="App"> <div className="container"> <h1> </div> </div> ); } const rootElement = document.getElementById("root"); ReactDOM.render(<App />, rootElement);Copy the code
As you can see, the root component is a function component of type App, so rootFiber is the parent node of App.
Combined with this analysis, _created4 in the figure is the Fiber node created according to the ReactElement corresponding to the first child node of rootFiber, so it is the Fiber node corresponding to App. Now print the _created4 value at run time, and you’ll see that it does:
The Fiber nodes that correspond to the App will be marked by the placeSingleChild with a “Placement” side effect, which is then returned as a function for reconcileChildFibers, Return to workinProgress.child as shown below:
The workInProgress in the context of the reconcileChildren function is the rootFiber node. Then, associate the newly created App Fiber node with rootFiber. The whole Fiber tree is shown as follows:
4) Sorting out the creation process of Fiber node
After analyzing the App FiberNode creation process, don’t rush down the render link. Because the most important stuff is already done, the creation of the remaining nodes is just a repetition of the related logic of performUnitOfWork, beginWork, and ChildReconciler.
The call stack involved in this analysis is very long, and many of you, if you are reading this for the first time, will inevitably have to look back repeatedly to see where you are on the call stack. Here, in order to facilitate the grasp of the logical context, the call process triggered by the beginWork explained in this lecture is summarized into a big picture:
2. Construction process of Fiber tree
Now that you understand how the Fiber node is created, it’s easy to understand how the Fiber tree is built.
Having studied the source logic of each key function with perseverance, you should be able to match the function name to the function’s work. Instead of worrying about the implementation details of the source code, we can look directly at the creation of subsequent nodes from a workflow perspective.
1) Loop to create a new Fiber node
Looking at the workflow created by the node, our starting point is the workLoopSync function.
Why did you choose it? Here’s a review of what workLoopSync does:
Function workLoopSync() {// If workInProgress is null while (workInProgress! // Execute performUnitOfWork(workInProgress) against it; }}Copy the code
It will loop through performUnitOfWork, which we mentioned in the beginning, and its main job is to “create a new Fiber node by calling the beginWork”; It also has a secondary job of updating the value of the newly created Fiber node into the workInProgress variable. The relevant logic in the source code is extracted as follows:
Next = beginWork$1(current, unitOfWork, subtreeRenderLanes); If (next === null) {// If this doesn't spawn new work, complete the current work. completeUnitOfWork(unitOfWork); } else { workInProgress = next; }Copy the code
This ensures that after each performUnitOfWork execution, the current workInProgress stores the next node that needs to be processed, ready for the next workLoopSync cycle.
Now create a breakpoint inside workLoopSync and try to print the value of workInProgress obtained each time. The value of workInProgress changes as shown in the following figure:
There are 7 nodes in total. If you click to expand the content of each node, you will find that the 7 nodes are actually:
-
RootFiber (the root node of the current Fiber tree)
-
App FiberNode (node corresponding to App function component)
-
Class is the node corresponding to THE DOM element of App, and its content is shown in the figure below
- Class is the node corresponding to the DOM element of the Container, as shown in the following figure
- Node corresponding to the H1 tag
FiberNode corresponding to the first P label contains “I am the first paragraph”, as shown in the following figure
The FiberNode corresponding to the second P label is “I am the second paragraph”, as shown in the following figure
Combine these 7 FiberNodes and compare with our Demo:
Function App() {return (<div className="App"> <div className="container"> <h1> </div> </div> ); }Copy the code
You’ll notice that from top to bottom, each non-text ReactElement has its corresponding Fiber node.
React does not create fiberNodes for all text types ReactElement, which is an optimization strategy. Whether FiberNode needs to be created is determined in the source by the isDirectTextChild variable.
As a result, fiberNodes are added to the tree, as shown below:
Fiber nodes are available, but how are they connected to each other?
2) How are Fiber nodes connected
The relationship between different Fiber nodes will be established through the attributes of child, return and Sibling, among which child and return record the parent-child node relationship, while Sibling record the sibling node relationship.
Here, take the Fiber node corresponding to the h1 element as an example to show how it is connected to other nodes. Expand the Fiber node and intercept its child, return and Sibling attributes, as shown below:
If the child attribute is null, the H1 node has no sub-fiber node:
Partial screenshot of return property:
Sibling attribute local screenshots:
As you can see, the return attribute points to the div node whose class is Container, and the Sibling attribute points to the first P node. Combining nested relationships in JSX is easy — in a FiberNode instance, return points to the parent of the current Fiber node, and Sibling points to the first sibling of the current node.
Combined with the relationship information recorded by these three attributes, the new FiberNodes combed out above can be easily connected:
This is the final form of the workInProgress Fiber tree. As you can see, while the product in question is still conventionally referred to as a “Fiber tree,” the nature of its data structure has changed from a tree to a linked list.
[Note] When analyzing the construction process of Fiber tree, we choose beginWork as the starting point, but in the whole construction process of Fiber tree, not only beginWork works. Interspersed with this is the work of the completeWork. Only when you look at the completeWork and beginWork side by side can you really understand what depth-first traversal in Fiber is all about.
3, summarize
Through the study of this lecture, we have mastered the realization principle of beginWork, clarified the creation link of Fiber node, and finally connected the macro construction process of Fiber tree. So far has captured the render stage half of the knowledge, this road is blocked and difficult, win in the harvest.
Next, on the one hand, we will continue to explore the work content of completeWork and explain the whole render stage thoroughly. On the other hand, we can quickly go through the workflow of commit stage, and connect the complete rendering workflow composed of initialization, render and COMMIT based on this, and strive to form a systematic and transparent understanding of the whole rendering link triggered by reactdom.render.
In addition, do you still remember the suspense left in the previous article: “Why need two Fiber trees” question? The answer to this question will emerge as we explore Fiber further.
4, the appendix
To study the source