React source code parsing 8. Render phase

Video courses (efficient Learning) :Into the curriculum

Course Contents:

1. Introduction and questions

2. React design philosophy

React source code architecture

4. Source directory structure and debugging

5. JSX & core API

Legacy and Concurrent mode entry functions

7. Fiber architecture

8. Render phase

9. The diff algorithm

10. com MIT stage

11. Life cycle

12. Status update process

13. Hooks the source code

14. Handwritten hooks

15.scheduler&Lane

16. Concurrent mode

17.context

18 Event System

19. Write the React mini

20. Summary & Answers to interview questions in Chapter 1

21.demo

Render stage entry

Render phase of the main work is to build a Fiber tree and generate effectList, we know the react in chapter 5 the two modes in the entrance goes performSyncWorkOnRoot or performConcurrentWorkOnRoot, These two methods call workLoopSync or workLoopConcurrent, respectively

//ReactFiberWorkLoop.old.js
function workLoopSync() {
  while(workInProgress ! = =null) { performUnitOfWork(workInProgress); }}function workLoopConcurrent() {
  while(workInProgress ! = =null&&! shouldYield()) { performUnitOfWork(workInProgress); }}Copy the code

If the browser doesn’t have enough time, it should terminate the while loop and not execute the render and commit phases. This part of scheduler’s knowledge is explained in Chapter 15.

  • WorkInProgress: The newly created workInProgress Fiber

  • The fiber tree will be connected to the created fiber tree. This process is similar to depth-first traversal, and we’ll tentatively call them ‘capture phase’ and ‘bubble phase’. The process of pseudo-code execution is roughly as follows

    function performUnitOfWork(fiber) {
      if (fiber.child) {
        performUnitOfWork(fiber.child);//beginWork
      }
    
      if (fiber.sibling) {
        performUnitOfWork(fiber.sibling);//completeWork}}Copy the code

The overall execution process of render stage

Watch video debugging with demo_0

  • The capture phase starts from the root node rootFiber and traverses to the leaf node. Each traversed node will execute beginWork and pass in the current Fiber node, then create or reuse its sub-fiber node and assign the value to workinProgress.child.

  • In the bubbling stage, after traversing to the child node in the capture stage, the completeWork method will be executed. After the completion of execution, the completeWork method will be judged whether the sibling node of this node exists. If so, completeWork will be executed for the sibling node. The completeWork is’ bubbled up ‘to the parent until rootFiber.

  • Example, demo_0 debugging

    function App() {
      return (
    		<>
          <h1>
            <p>count</p> xiaochen
          </h1>
        </>
      )
    }
    
    ReactDOM.render(<App />.document.getElementById("root"));
    Copy the code

    Fiber tree formed after depth-first traversal:

The numbers in the figure are the sequence in the traversal process. It can be seen that in the traversal process, the beginWork and completeWork are executed successively from the root node of the application rootFiber, and finally a Fiber tree is formed. Each node is connected with child and return.

Note: When traversing Fiber with only one child text node, the children of the Fiber node do not execute beginWork and completeWork, as shown in the ‘Chen’ text node. This is an optimization of React

beginWork

The beginWork’s main job 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 under construction
  renderLanes: Lanes,// Chapter 12
) :Fiber | null {
 / / 1. When the update satisfy conditions can reuse current fiber into bailoutOnAlreadyFinishedWork 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;
  }

  The reconcileChildren function is included in the reconcileChildren function
  switch (workInProgress.tag) {
    case IndeterminateComponent: 
      // ...
    case LazyComponent: 
      // ...
    case FunctionComponent: 
      // ...
    case ClassComponent: 
      // ...
    case HostRoot:
      // ...
    case HostComponent:
      // ...
    case HostText:
      // ...}}Copy the code

As can be seen from the code, the parameter contains current Fiber, which is the Fiber tree corresponding to the current real DOM. In the previous introduction of Fiber double-cache mechanism, we know that current is equal to null except rootFiber when rendering for the first time, because the DOM has not been constructed for the first time. The beginWork function uses current === null to judge whether the dom tree is mounted or updated. The beginWork function uses current === null to judge whether the DOM tree is mounted or updated

  • Mount: The reconcileChildren creation functions that enter the different fibers according to the Fiber. tag are finally called to create sub-fibers
  • Update: When constructing workInProgress when meet the conditions, can reuse current Fiber to optimize, namely into bailoutOnAlreadyFinishedWork logic, can reuse didReceiveUpdate variable is false, The condition for reuse is
    1. The oldProps == newProps && workinProgress. type === current
    2. ! IncludesSomeLane (renderLanes, updateLanes) whether the priorities of the updates are adequate is explained in Chapter 15

reconcileChildren/mountChildFibers

The process of creating sub-fibers for the reconcileChildren goes into the reconcileChildren, and the function is to generate its child fiber, or WorkInProgress.Child, for the workInProgress Fiber node. It then proceeds with depth-first traversal of its children to perform the same operation.

//ReactFiberBeginWork.old.js
export function reconcileChildren(
  current: Fiber | null,
  workInProgress: Fiber,
  nextChildren: any,
  renderLanes: Lanes
) {
  if (current === null) {
    / / the mount
    workInProgress.child = mountChildFibers(
      workInProgress,
      null,
      nextChildren,
      renderLanes,
    );
  } else {
    //updateworkInProgress.child = reconcileChildFibers( workInProgress, current.child, nextChildren, renderLanes, ); }}Copy the code

The reconcileChildren distinguishes between mount and Update, entering reconcileChildFibers or mountChildFibers, The reconcileChildFibers and mountChildFibers are ultimately functions that ChildReconciler returns with the different arguments it passes to determine whether to track side effects, In ChildReconciler, shouldTrackSideEffects is used to determine whether effectTag should be added to the corresponding node. For example, if a node needs to be inserted, two conditions should be met:

  1. fiber.stateNode! ==null indicates that fiber has a real DOM and the real DOM is stored on the stateNode

  2. (fiber.effectTag & Placement) ! == 0 Fiber has Placement effectTag

    var reconcileChildFibers = ChildReconciler(true);
    var mountChildFibers = ChildReconciler(false);
    Copy the code
    function ChildReconciler(shouldTrackSideEffects) {
    	function placeChild(newFiber, lastPlacedIndex, newIndex) {
        newFiber.index = newIndex;
    
        if(! shouldTrackSideEffects) {// Whether to track side effects
          // 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.
            returnoldIndex; }}else {
          // This is an insertion.
          newFiber.flags = Placement;
          returnlastPlacedIndex; }}}Copy the code

In the previous introduction to the mental model, we know that after putting an effectTag on the Fiber, the DOM is added, deleted, and altered in the COMMIT phase, and that there is alternate rootFiber in the reconcileChildren. That is, rootFiber has a corresponding current Fiber, so rootFiber walks the reconcileChildFibers’ logic, so shouldTrackSideEffects equals true tracks side effects, Finally, put a Placement effectTag on rootFiber and then insert the DOM once to improve performance.

export const NoFlags = / * * / 0b0000000000000000000;
/ / insert the dom
export const Placement = / * * / 0b00000000000010;
Copy the code

In the reactFiberFlags. js file of the source code, use binary operations to determine whether Placement exists, such as var a = NoFlags. If you need to add a Placement effectTag to a, Just as long as effectTag | Placement is ok

bailoutOnAlreadyFinishedWork

//ReactFiberBeginWork.old.js
function bailoutOnAlreadyFinishedWork(current, workInProgress, renderLanes) {
  
  / /...
	if(! includesSomeLane(renderLanes, workInProgress.childLanes)) {return null;
    
  } else {
    
    cloneChildFibers(current, workInProgress);
    
    returnworkInProgress.child; }}Copy the code

If entered bailoutOnAlreadyFinishedWork multiplexing logic, chapter 12 will determine the priority, priority is enough to enter cloneChildFibers otherwise returns null

completeWork

The completeWork handles the props of fiber, creates the DOM, and creates the 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, HostComponent, and 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 {
        / / the mount
        const currentHostContext = getHostContext();
        // Create the dom node corresponding to fiber
        const instance = createInstance(
            type,
            newProps,
            rootContainerInstance,
            currentHostContext,
            workInProgress,
          );
        // Insert descendant DOM nodes into the newly created DOM
        appendAllChildren(instance, workInProgress, false.false);
        // Dom node is assigned to fiber.statenode
        workInProgress.stateNode = instance;

        // The processing props are similar to updateHostComponent
        if( finalizeInitialChildren( instance, type, newProps, rootContainerInstance, currentHostContext, ) ) { markUpdate(workInProgress); }}return null;
    }
Copy the code

As you can see from the simplified version of completeWork, this function does a couple of things

  • Enter different functions based on workinProgress. tag. Let’s use HostComponent as an example
  • Update workinprogress. stateNode== null update workinprogress. stateNode== null Call updateHostComponent to handle props (including onClick, style, children…) , and will deal with good props to updatePayload, finally will hold in workInProgress. UpdateQueue
  • Call createInstance to create the DOM, insert the descendant DOM node into the dom, and call finalizeInitialChildren props (same logic as updateHostComponent)

Before we mentioned that when the beginWork is mounted, rootFiber has the corresponding current, so it will execute mountChildFibers and put the Placement effectTag. In the bubble stage, which is the completeWork, We attach the descendant node to the newly created DOM node through appendAllChildren, and finally we can reflect the in-memory node into the real DOM at once using the DOM native method.

In the beginWork phase, some nodes are marked with effectTags, and some are not. In the commit phase, all Fiber nodes with EffectTags are added, deleted, and modified. The answer is no, it’s space for time, and I’m executing the completeWork and I’m hitting an effectTag, Will this node to join a call effectList, so at the commit stage just traverse effectList (rootFiber. FirstEffect. NextEffect can access with effectTag Fiber)

The pointer operation to the effectList occurs in the completeUnitOfWork function, as in our application

function App() {
  
  const [count, setCount] = useState(0);
  
  return (
    
   	 <>
      <h1
        onClick={()= > {
          setCount(() => count + 1);
        }}
      >
        <p title={count}>{count}</p> xiaochen
      </h1>
    </>)}Copy the code

So our operation effectList pointer is as follows (this graph is the graph during operation pointer, at this time app Fiber node is traversed, when traversing rootFiber, H1, P nodes and rootFiber will form a circular linked list)

rootFiber.firstEffect===h1

rootFiber.firstEffect.next===p
Copy the code

Forming a circular list merges the effectList up from the node that triggered the update to the rootFiber. This happens in the completeUnitOfWork function, which merges the effectList up

//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 parent's effectList header pointer points to the effectList header pointer of the completedWork
        }
        if(completedWork.lastEffect ! = =null) {
          if(returnFiber.lastEffect ! = =null) {
            // The parent's effectList header pointer points to the effectList header pointer of the completedWork
            returnFiber.lastEffect.nextEffect = completedWork.firstEffect;
          }
          // The effectList tail pointer for the parent nod points to the effectList tail pointer for the completedWork
          returnFiber.lastEffect = completedWork.lastEffect;
        }

        const flags = completedWork.flags;
        if (flags > PerformedWork) {
          if(returnFiber.lastEffect ! = =null) {
            // The completedWork itself is appended to the end of the effectList in the returnFiber
            returnFiber.lastEffect.nextEffect = completedWork;
          } else {
            //returnFiber's effectList header points to completedWork
            returnFiber.firstEffect = completedWork;
          }
          //returnFiber's effectList terminator points to completedWorkreturnFiber.lastEffect = completedWork; }}}else {

      / /...

      if(returnFiber ! = =null) {
        returnFiber.firstEffect = returnFiber.lastEffect = null;/ / remakes effectListreturnFiber.flags |= Incomplete; }}}while(completedWork ! = =null);

	/ /...
}
Copy the code

The resulting fiber tree is as follows

Then commitRoot (root); Entering the Commit phase