React source code

6. Render phase (Cool, I have the skill to create Fiber)

Video lessons & Debug demos

The purpose of the video course is to quickly master the process of running the React source code and scheduler, Reconciler, Renderer, Fiber, etc., in react, and debug the source code and analysis in detail to make the process clearer.

Video course: Enter the course

Demos: demo

Course Structure:

  1. You’re still struggling with the React source code.
  2. The React Mental Model
  3. Fiber(I’m dom in memory)
  4. Start with Legacy or Concurrent (start at the entrance and let’s move on to the future)
  5. State update process (what’s going on in setState)
  6. Render phase (awesome, I have the skill to create Fiber)
  7. Commit phase (I heard renderer marked it for us, let’s map real nodes)
  8. Diff algorithm (Mom doesn’t worry about my Diff interviews anymore)
  9. Function Component saves state
  10. Scheduler&lane model (to see tasks are paused, continued, and queue-jumped)
  11. What is concurrent mode?
  12. Handwriting mini React (Short and Sharp is me)

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

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 covered in Chapter 12.

  • 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 execution process 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 the breakpoint debug video for more details on function execution:

Use demo_0 to see the execution

  • Capture phase

    Starting from the root node rootFiber, traversal to the leaf node, each traversal will execute beginWork and pass in the current Fiber node, then create or reuse its child Fiber node and assign the value to workinProgress.child.

  • Bubbling phase

    In the capture stage, after traversing to the child node, the completeWork method will be executed. After completion, the completeWork method will be judged whether the sibling node of this node exists. If it exists, completeWork will be executed for the sibling node. Until rootFiber.

  • The sample

function App() {
  return (
    <div>
      xiao
      <p>chen</p>
    </div>
  )
}

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 a Fiber with only one child 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 12

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.

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! (Fiber. EffectTag & Placement)! == 0 Fiber has Placement effectTagCopy the code
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

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

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 (
        <div className="App">
          <p onClick={()= > setCount(() => count + 1)}>
            <h1 title={count}>{count}</h1> and save to reload.
          </p>
        </div>
      );
    }
    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

    The resulting fiber tree is as follows

    Then commitRoot (root); Entering the Commit phase