Talk about react -- what happens after status update

Learning the bottom process of react is the same as learning other principles. Grasping some key points, that is, key functions (often representing some stages), can make the grasp of the source code and the graphical process clearer and easier to understand.

 

We know that the facebook team has made some major refactoring of the bottom layer of react after react16. A big white line is to let react realize asynchronous and interruptible updates.

  As for the implementation, react introduces the Scheduler scheduler, which will allocate an initial execution time to the js thread. In the source code, yieldInterval=5ms. If the reserved time is not enough for the browser to render, react will give control to the browser and wait until the next frame. The specific principles will not be discussed in detail in this chapter.

However, not all phases of the update process are asynchronous, which can cause some unexpected problems

Back to the status update, you can change the status. There are probably these:

  • ReactDOM.render

  • this.setState

  • this.forceUpdate

  • useState

  • useReducer

Obviously, react developers will definitely think of accessing a set of state update system. How to realize it? The answer is to create an object to save the state update each time, and calculate the new state according to the object in the render phase. However, in the render stage, the traversal starts from rootFiber. The update object may be a node in the fiber tree. There must be a way to make react return to the vertex from the node. The key process is the markUpdateLaneFromFiberToRoot function.

function markUpdateLaneFromFiberToRoot(
  sourceFiber: Fiber,
  lane: Lane,
): FiberRoot | null {
  // Update the source fiber's lanes
  sourceFiber.lanes = mergeLanes(sourceFiber.lanes, lane);
  let alternate = sourceFiber.alternate;
  if (alternate !== null) {
    alternate.lanes = mergeLanes(alternate.lanes, lane);
  }
    let node = sourceFiber;
  let parent = sourceFiber.return;
  while (parent !== null) {
    parent.childLanes = mergeLanes(parent.childLanes, lane);
    alternate = parent.alternate;
    if (alternate !== null) {
      alternate.childLanes = mergeLanes(alternate.childLanes, lane);
    }
    }
    node = parent;
    parent = parent.return;
  }
  if (node.tag === HostRoot) {
    const root: FiberRoot = node.stateNode;
    return root;
  } else {
    return null;
  }
}

This is the source code. It can be seen that it receives not only a fiber but also a priority. This also corresponds to that update has not only one-dimensional state saving, but also a priority. Logically, it means constantly searching through the return pointer to the upper layer until the top layer is found.

If you have rootfiber to update immediately, there must be a problem. That is, updates are synchronous, but react is asynchronous, so you have to schedule updates. The core function is ensureroot isscheduled.

The core code is as follows:

if (newCallbackPriority === SyncLanePriority) {
  //Synchronize high priority tasks
  newCallbackNode = scheduleSyncCallback(
  //Execute render phase synchronously
    performSyncWorkOnRoot.bind(null, root)
  );
} else {
  // Asynchronous low priority task
  var schedulerPriorityLevel = lanePriorityToSchedulerPriority(
    newCallbackPriority
  );
  newCallbackNode = scheduleCallback(
    schedulerPriorityLevel,
   Asynchronous execution render stage
    performConcurrentWorkOnRoot.bind(null, root)
  );
}

  In this way, the render phase can be asynchronous, synchronous and priority, and then it can be updated openly.  

Render phase       

Note that the render phase begins with a call to the performSyncWorkOnRoot or performcurrentworkonroot methods.

// performSyncWorkOnRoot will call this method -- synchronous update
function workLoopSync() {
  while (workInProgress !== null) {
    performUnitOfWork(workInProgress);
  }
}

// Performcurrentworkonroot calls this method -- asynchronous update
function workLoopConcurrent() {
  while (workInProgress !== null && !shouldYield()) {
    performUnitOfWork(workInProgress);
  }
}

  Note that there is a key function shouldYield() in asynchronous update. This function will monitor whether the browser has time left in the current frame. If not, it will terminate the update and wait until the next frame.

workInProgress represents the fiber tree being built, and performnunitofwork will create the next fiber node and connect the previous node to form a fiber tree.

In the whole render stage, it can also be divided into two important periods, corresponding to the process that the fiber is traversed twice. In the first stage, beginWork() starts from the rootfiber node and traverses downward. When the leaf node is traversed, the completeWork() function will be executed step by step from the leaf node to the rootfiber.

Let's talk about beginWork in detail. The function is very simple. It is to create a child fiber node according to the incoming fiber node, and label the node with some effectTag tags by comparing the old and new nodes for processing in the commit stage. In general, it is divided into two stages:

1.mount, because the current tree is still empty, all values will generate corresponding fiber nodes according to different fiber.tag s.

 switch (workInProgress.tag) {
    case IndeterminateComponent: 
      // ... omitted
    case LazyComponent: 
      // ... omitted
    case FunctionComponent: 
      // ... omitted
    case ClassComponent: 
      // ... omitted
    case HostRoot:
      // ... omitted
     .
     .
     .
}

2. The situation will be more complicated during update. In this process, diff will be performed by calling the reconcileChildren method to try to reuse the current node as much as possible.  

//reconcileChildren partial logic
if (current === null) {
    // For mount components
    workInProgress.child = mountChildFibers(
      workInProgress,
      null,
      nextChildren,
      renderLanes,
    );
  } else {
    // For update components
    workInProgress.child = reconcileChildFibers(
      workInProgress,
      current.child,
      nextChildren,
      renderLanes,
    );
  }

In fact, reconcileChildren is called when mount and update. But why call two different functions?

Because the fiber nodes in the mount phase are created for the first time, if they are labeled with effectTag, the performance will be affected. Therefore, when mounting, mountchildFibers will only mark the rootFiber node with a placement mark and join it at one time.

The next step is the complete work (current, work in progress) stage. The role of this stage is summarized as mount

  Create a DOM node according to fiber and initialize the props of the DOM node. The update stage is to finish the work after diff, and form an array of props to be updated according to the fiber of the effectTag tag.

Important functions in mount phase and update phase:

const currentHostContext = getHostContext();
// Create the corresponding DOM node for fiber
const instance = createInstance(
    type,
    newProps,
    rootContainerInstance,
    currentHostContext,
    workInProgress,
  );

// Insert the DOM node into the newly generated DOM node
appendAllChildren(instance, workInProgress, false, false);

// The DOM node is assigned to fiber.stateNode
workInProgress.stateNode = instance;

The mount phase also initializes props on the DOM node.

 // update
  updateHostComponent(
    current,
    workInProgress,
    type,
    newProps,
    rootContainerInstance,
  );
//Inside updateHostComponent
workInProgress.updateQueue = (updatePayload: any);

updatePayload is the props array to be updated.

Finally, there is a small detail in the completeWork stage. In order to make the commit stage operate DOM more quickly, an effectList array will be generated in the completeWork homecoming stage, which contains the DOM to be updated in the next stage.

At this point, the render phase is completed. This process may not be smooth. It will be terminated and restored due to high priority and browser rendering time. The currentFiber has not changed yet. What really changes the DOM is the next phase, which is also the synchronous commit phase.

The entry function of commit is commitRoot(root)

This stage is roughly divided into three stages:

before mutation stage----mutation stage----layout stage

1.befor mutation stage.

It is mainly to traverse the effectList and call the main function commitBeforeMutationEffects. In this function, the hook getSnapshotBeforeUpdate will be called to start. It can also be seen that it is executed in the synchronization phase and before componentDidMount. The second is to schedule useEffect, which is an important play.

// Scheduling useEffect
if ((effectTag & Passive) !== NoEffect) {
  if (!rootDoesHavePassiveEffects) {
    rootDoesHavePassiveEffects = true;
    scheduleCallback(NormalSchedulerPriority, () => {
      // Trigger useEffect
      flushPassiveEffects();
      return null;
    });
  }
}

  Let's talk about the scheduling process of useEffect. useEffect schedules useEffect through flushpassive effects at this stage, but it will not be executed immediately, because it is an asynchronous process. The parameter rootwithpendingpassive effects that flushpassive effects depends on is null at this time. Rootwithpendingpassive effects will not be assigned to effectList until the layout stage, Execute callback, which is also the principle of useEffect asynchronous callback.

And for this reason, useEffect performs better than   After componentDidMount and componentDidUpdate are executed, and the view has been updated, the page rendering will not be blocked.

2.mutation stage.

The process of actually performing DOM operations. This stage also traverses the effectList, and the main function is commitMutationEffects. Receive a fiberRoot and update level as parameters.

function commitMutationEffects(root: FiberRoot, renderPriorityLevel) {
  // Traverse the effectList
  while (nextEffect !== null) {

    const effectTag = nextEffect.effectTag;
    .
    .
    .
}
  • Recharge text nodes. In react, text nodes are processed separately
  • Update ref
  • Perform different operations according to different effectTag s, including Placement, update, deletion, and hydrogenation.

  Let's talk about the specific effectTag. The Placement tag calls the commitPlacement function to obtain the parent fiber node, and then insert the DOM node with the DOM operation of insertBefore or appendChild. When the fiber contains Update effectTag, commitUpdate will be called to execute the destruction function of uselayouteeffect of the function component. For the native DOM component, the content corresponding to updateQuene attached to the fiber node in the completeWork stage will be rendered to the page. As for the Deletion effectTag, delete the fiber node, unbind the ref, and execute compoentWillUnmount. And the destruction function of useEffect will be executed.

3.layout stage.

Like the previous phase, this phase will traverse the effectList. The DOM structure at this stage has been updated and the rendering is complete. The main function at this stage is commitLayoutEffects. The main logic codes are as follows:

function commitLayoutEffects(root: FiberRoot, committedLanes: Lanes) {
  while (nextEffect !== null) {
    const effectTag = nextEffect.effectTag;

    // Call lifecycle hooks and hooks
    if (effectTag & (Update | Callback)) {
      const current = nextEffect.alternate;
      commitLayoutEffectOnFiber(root, current, nextEffect, committedLanes);
    }

    // Assignment ref
    if (effectTag & Ref) {
      commitAttachRef(nextEffect);
    }

    nextEffect = nextEffect.nextEffect;
  }
}

  1.commitLayoutEffectOnFiber (the previous version was called commitlife cycles). For class components, the componentdidcount and componentDidupdate functions will be executed according to current==null. And at this time, the second parameter callback function of setState will be executed here. For function components, it will call the callback function of uselayouteeffect and the destruction and callback function of scheduling useEffect. Note that useEffect is not executed at this time, and it will be executed after the layout stage.

2. Committatchref. This function is relatively simple. Get the DOM instance of the instance, and then update Ref. if ref is a return function, it will also be executed. If it is an object instance, it will be assigned directly.

So far, the whole process is almost over, but when did the two fiber trees in memory switch?

The answer is: between the mutation and layout phases.

Summarize the whole process from status update to page display:

  • Call different methods to update the status
  • Create Update object
  • Call markUpdateLaneFromFiberToRoot to return to the root node
  • Call ensureroot isscheduled to update the schedule
  • render stage
  • commit phase

This is just to look at the whole update process from the perspective of the underlying principle and process. react also has many places worth studying in this process, such as using bit operation and bit identification, and using an odd even number of an array to save the opposite callback function... But it should also explain some processes. There is no end to learning. Come on!

If there is something wrong, please point out and make common progress!!!

Tags: React

Posted on Sat, 11 Sep 2021 13:56:39 -0400 by newb