react source code analysis 8.render phase
Video Explanation (efficient learning): Enter learning
Previous articles:
1. Introduction and interview questions
3.react source code architecture
4. Source directory structure and debugging
6.legacy and concurrent mode entry functions
20. Summary & answers to interview questions in Chapter 1
Entry to render stage
The main work of the render phase is to build the Fiber tree and generate the effectList. In Chapter 5, we know that the two modes of the react entry will enter performSyncWorkOnRoot or performcurrentworkonroot, and the two methods will call workLoopSync or workLoopConcurrent respectively
//ReactFiberWorkLoop.old.js function workLoopSync() { while (workInProgress !== null) { performUnitOfWork(workInProgress); } } function workLoopConcurrent() { while (workInProgress !== null && !shouldYield()) { performUnitOfWork(workInProgress); } }
The difference between the two functions is to judge whether there is shouldYield execution of the condition. If the browser does not have enough time, the while loop will be terminated, and the later performnunitofwork function will not be executed. Naturally, the later render stage and commit stage will not be executed. This part belongs to the knowledge point of the scheduler, which we will explain in Chapter 15.
- workInProgress: the newly created workInProgress fiber
Performnunitofwork: workInProgress fiber and will be connected with the created Fiber to form a Fiber tree. This process is similar to depth first traversal. We call them "capture stage" and "bubble stage" for the time being. The process of pseudo code execution is as follows
function performUnitOfWork(fiber) { if (fiber.child) { performUnitOfWork(fiber.child);//beginWork } if (fiber.sibling) { performUnitOfWork(fiber.sibling);//completeWork } }
Overall execution process of render stage
Use demo_0 Watch Video debugging
- Capture phase
Start from the root node rootFiber and traverse to the leaf node. Each traversed node will execute beginWork and pass in the current Fiber node, then create or reuse its child Fiber node and assign it to workInProgress.child. - bubbling phase
After traversing the child node in the capture phase, the completeWork method will be executed. After execution, it will judge whether the sibling node of this node exists. If it exists, completeWork will be executed for the sibling node. After all sibling nodes are executed, it will bubble up to the parent node to execute completeWork until rootFiber. Example, demo_0 debugging
function App() { return ( <> <h1> <p>count</p> xiaochen </h1> </> ) } ReactDOM.render(<App />, document.getElementById("root"));
Fiber tree formed after depth first traversal:
The numbers in the figure are the order in the traversal process. You can see that the traversal process will start from the root node rootFiber of the application, execute beginWork and completeWork in turn, and finally form a Fiber tree, with each node connected by child and return.
Note: when traversing a Fiber with only one child text node, the child nodes of the Fiber node will not execute beginWork and completeWork, as shown in the 'chen' text node in the figure. This is an optimization method of react
beginWork
The main work of beginWork is to create or reuse sub fiber nodes
function beginWork( current: Fiber | null,//The corresponding Fiber tree currently exists in the dom tree workInProgress: Fiber,//Fiber tree being built renderLanes: Lanes,//Chapter 12 is about ): Fiber | null { // 1. When the conditions are met during update, reuse the current fiber and enter the bailoutonalreadyfinished work function if (current !== null) { const oldProps = current.memoizedProps; const newProps = workInProgress.pendingProps; if ( oldProps !== newProps || hasLegacyContextChanged() || (__DEV__ ? workInProgress.type !== current.type : false) ) { didReceiveUpdate = true; } else if (!includesSomeLane(renderLanes, updateLanes)) { didReceiveUpdate = false; switch (workInProgress.tag) { // ... } return bailoutOnAlreadyFinishedWork( current, workInProgress, renderLanes, ); } else { didReceiveUpdate = false; } } else { didReceiveUpdate = false; } //2. Create different fiber s according to the tag, and finally enter the reconcileChildren function switch (workInProgress.tag) { case IndeterminateComponent: // ... case LazyComponent: // ... case FunctionComponent: // ... case ClassComponent: // ... case HostRoot: // ... case HostComponent: // ... case HostText: // ... } }
We can see from the code that there is current Fiber in the parameter, that is, the Fiber tree corresponding to the current real dom. In the Fiber dual cache mechanism introduced earlier, we know that in the first rendering, except rootFiber, current is equal to null, because the DOM has not been constructed in the first rendering, and current is not equal to null in update, because the DOM tree already exists in update, Therefore, the beginWork function uses current === null to determine whether mount or update enters different logic
- mount: enter the creation functions of different fibers according to fiber.tag, and finally call reconcileChildren to create child fibers
update: when building workInProgress, when the conditions are met, the current Fiber will be reused for optimization, that is, enter the logic of bailoutonalreadyfinished work. The variable of didReceiveUpdate that can be reused is false, and the reuse condition is
- Oldprops = = = newprops & & workinprogress. Type = = = the current.type attribute and the type of fiber remain unchanged
- ! Whether the update priority of includesomelane (render lanes, update lanes) is sufficient is explained in Chapter 15
reconcileChildren/mountChildFibers
The process of creating a child fiber will enter reconcileChildren. This function is used to generate its child fiber, workInProgress.child, for the workInProgress fiber node. Then continue to depth first traverse its child nodes to perform the same operation.
//ReactFiberBeginWork.old.js export function reconcileChildren( current: Fiber | null, workInProgress: Fiber, nextChildren: any, renderLanes: Lanes ) { if (current === null) { //mount time workInProgress.child = mountChildFibers( workInProgress, null, nextChildren, renderLanes, ); } else { //update workInProgress.child = reconcileChildFibers( workInProgress, current.child, nextChildren, renderLanes, ); } }
reconcileChildren will distinguish between mount and update and enter reconcileChildFibers or mountChildFibers. reconcileChildFibers and mountChildFibers are actually the functions returned by ChildReconciler passing different parameters. This parameter is used to indicate whether to track side effects, In ChildReconciler, use shouldTrackSideEffects to determine whether to mark the corresponding node with effectTag. For example, if a node needs to be inserted, two conditions need to be met:
- fiber.stateNode!==null means that the fiber has a real dom, which is saved on the statenode
(fiber.effectTag & Placement) !== 0 fiber has an effecttag for placement
var reconcileChildFibers = ChildReconciler(true); var mountChildFibers = ChildReconciler(false);
```js function ChildReconciler(shouldTrackSideEffects) { function placeChild(newFiber, lastPlacedIndex, newIndex) { newFiber.index = newIndex; if (!shouldTrackSideEffects) {//Are side effects tracked // Noop. return lastPlacedIndex; } var current = newFiber.alternate; if (current !== null) { var oldIndex = current.index; if (oldIndex < lastPlacedIndex) { // This is a move. newFiber.flags = Placement; return lastPlacedIndex; } else { // This item can stay in place. return oldIndex; } } else { // This is an insertion. newFiber.flags = Placement; return lastPlacedIndex; } } } ```
In the previous introduction to the mental model, we know that the addition, deletion and modification of the corresponding dom will be performed in the commit phase after the effectTag is marked for the Fiber, and there is an alternate rootFiber in the reconcileChildren, that is, there is a corresponding current Fiber in the rootFiber, so the rootFiber will follow the logic of reconcileChildFibers, Therefore, shouldTrackSideEffects equal to true will track side effects. Finally, put the effectTag of Placement on rootFiber, and then insert dom at one time to improve performance.
export const NoFlags = /* */ 0b0000000000000000000; // Insert dom export const Placement = /* */ 0b00000000000010;
In the ReactFiberFlags.js file of the source code, binary operation is used to judge whether there is Placement. For example, let var a = NoFlags. If you need to add the effectag of Placement on a, you can just use effectag | Placement
bailoutOnAlreadyFinishedWork
//ReactFiberBeginWork.old.js function bailoutOnAlreadyFinishedWork(current, workInProgress, renderLanes) { //... if (!includesSomeLane(renderLanes, workInProgress.childLanes)) { return null; } else { cloneChildFibers(current, workInProgress); return workInProgress.child; } }
If the logic of bailoutonalreadyfinished work reuse is entered, the priority will be determined. As described in Chapter 12, if the priority is sufficient, enter cloneChildFibers, otherwise null will be returned
completeWork
completeWork mainly deals with fiber props, creating dom and creating effectList
//ReactFiberCompleteWork.old.js function completeWork( current: Fiber | null, workInProgress: Fiber, renderLanes: Lanes, ): Fiber | null { const newProps = workInProgress.pendingProps; //Enter different logic according to workInProgress.tag. Here we focus on HostComponent and HostComponent. We will talk about other types later switch (workInProgress.tag) { case IndeterminateComponent: case LazyComponent: case SimpleMemoComponent: case HostRoot: //... case HostComponent: { popHostContext(workInProgress); const rootContainerInstance = getRootHostContainer(); const type = workInProgress.type; if (current !== null && workInProgress.stateNode != null) { // update updateHostComponent( current, workInProgress, type, newProps, rootContainerInstance, ); } else { // mount time const currentHostContext = getHostContext(); // Create the dom node corresponding to fiber const instance = createInstance( type, newProps, rootContainerInstance, currentHostContext, workInProgress, ); // Insert the descendant DOM node into the newly created dom appendAllChildren(instance, workInProgress, false, false); // The dom node is assigned to fiber.stateNode workInProgress.stateNode = instance; // Handling props is similar to updateHostComponent if ( finalizeInitialChildren( instance, type, newProps, rootContainerInstance, currentHostContext, ) ) { markUpdate(workInProgress); } } return null; }
As you can see from the simplified version of completeWork, this function does the following things
- Enter different functions according to workInProgress.tag. Let's take HostComponent as an example
- When updating (in addition to current===null, you also need to judge workInProgress.stateNode===null), call updateHostComponent to process props (including onClick, style, children...), assign the processed props to updatepapayload, and finally save it on workInProgress.updateQueue
- mount calls createInstance to create dom, inserts the descendant dom node into the newly created dom, and calls finalizeInitialChildren to process props (similar to the logic of updateHostComponent processing).
We mentioned earlier that during the mount of beginWork, the rootFiber has a corresponding current, so it will execute the effectTag of mountChildFibers and Placement. In the bubble stage, that is, when executing completeWork, we will mount the descendant node to the newly created dom node through appendAllChildren, Finally, the nodes in memory can be reflected into the real dom at one time using the dom native method.
In beginWork, we know that some nodes are marked with effectTag and some are not. In the commit phase, we need to traverse all fibers containing effectTag to perform corresponding additions, deletions and modifications. We also need to find these nodes with effectTag from the Fiber tree. The answer is not necessary. Here is to trade space for time, When a node with effectTag is encountered during the execution of completeWork, this node will be added to an effectList. Therefore, in the commit phase, you only need to traverse the effectList (rootFiber.firstEffect.nextEffect can access the Fiber with effectTag)
The pointer operation of effectList occurs in the completeUnitOfWork function. For example, our application is like this
function App() { const [count, setCount] = useState(0); return ( <> <h1 onClick={() => { setCount(() => count + 1); }} > <p title={count}>{count}</p> xiaochen </h1> </> ) }
Then our operation effectList pointer is as follows (this figure is a diagram of the operation pointer process. At this time, the app Fiber node is traversed. When the rootFiber node is traversed, the h1 and p nodes will form a circular linked list with the rootFiber)
rootFiber.firstEffect===h1 rootFiber.firstEffect.next===p
When forming a circular linked list, the effectList will be merged upward from the node triggering the update to rootFiber. This process occurs in the completeUnitOfWork function. The function is to merge the effectList upward
//ReactFiberWorkLoop.old.js function completeUnitOfWork(unitOfWork: Fiber): void { let completedWork = unitOfWork; do { //... if ( returnFiber !== null && (returnFiber.flags & Incomplete) === NoFlags ) { if (returnFiber.firstEffect === null) { returnFiber.firstEffect = completedWork.firstEffect;//The effectList header pointer of the parent node points to the effectList header pointer of completedWork } if (completedWork.lastEffect !== null) { if (returnFiber.lastEffect !== null) { //The header and tail pointers of the effectList of the parent node point to the effectList header pointer of the completedWork returnFiber.lastEffect.nextEffect = completedWork.firstEffect; } //The end pointer of the effectList nodded by the parent node points to the end pointer of the effectList of the completedWork returnFiber.lastEffect = completedWork.lastEffect; } const flags = completedWork.flags; if (flags > PerformedWork) { if (returnFiber.lastEffect !== null) { //completedWork itself is appended to the end of the effectList of returnFiber returnFiber.lastEffect.nextEffect = completedWork; } else { //The effectList header node of returnFiber points to completedWork returnFiber.firstEffect = completedWork; } //The effect list tail node of returnFiber points to completedWork returnFiber.lastEffect = completedWork; } } } else { //... if (returnFiber !== null) { returnFiber.firstEffect = returnFiber.lastEffect = null;//Remake effectList returnFiber.flags |= Incomplete; } } } while (completedWork !== null); //... }
Finally, the generated fiber tree is as follows
Then commit root (root); Enter the commit phase