react source code analysis 12. Status update process

Keywords: Front-end React

react source code analysis 12. Status update process

Video Explanation (efficient learning): Enter learning

Previous articles:

1. Introduction and interview questions

2. Design concept of react

3.react source code architecture

4. Source directory structure and debugging

5. JSX & Core api

6.legacy and concurrent mode entry functions

7.Fiber architecture

8.render stage

9.diff algorithm

10.commit phase

11. Life cycle

12. Status update process

13.hooks source code

14. Handwritten hooks

15.scheduler&Lane

16.concurrent mode

17.context

18 event system

19. Handwritten Mini react

20. Summary & answers to interview questions in Chapter 1

setState&forceUpdate

Several ways to trigger status update in react:

  • ReactDOM.render
  • this.setState
  • this.forceUpdate
  • useState
  • useReducer

Let's focus on this.setState and this.forceUpdate. hook is described in Chapter 13

  1. this.updater.enqueueSetState is called in this.setState, mainly to add update to updateQueue

    //ReactBaseClasses.js
    Component.prototype.setState = function (partialState, callback) {
      if (!(typeof partialState === 'object' || typeof partialState === 'function' || partialState == null)) {
        {
          throw Error( "setState(...): takes an object of state variables to update or a function which returns an object of state variables." );
        }
      }
      this.updater.enqueueSetState(this, partialState, callback, 'setState');
    };
    
    
    //ReactFiberClassComponent.old.js
    enqueueSetState(inst, payload, callback) {
      const fiber = getInstance(inst);//fiber instance
    
      const eventTime = requestEventTime();
      const suspenseConfig = requestCurrentSuspenseConfig();
      
      const lane = requestUpdateLane(fiber, suspenseConfig);//priority
    
      const update = createUpdate(eventTime, lane, suspenseConfig);//Create update
    
      update.payload = payload;
    
      if (callback !== undefined && callback !== null) {  //Assignment callback
        update.callback = callback;
      }
    
      enqueueUpdate(fiber, update);//update join updateQueue
      scheduleUpdateOnFiber(fiber, lane, eventTime);//Schedule update
    }
    

    enqueueUpdate is used to add update to the updateQueue queue

    //ReactUpdateQueue.old.js
    export function enqueueUpdate<State>(fiber: Fiber, update: Update<State>) {
      const updateQueue = fiber.updateQueue;
      if (updateQueue === null) {
        return;
      }
    
      const sharedQueue: SharedQueue<State> = (updateQueue: any).shared;
      const pending = sharedQueue.pending;
      if (pending === null) {
        update.next = update;//Form a circular linked list with yourself
      } else {
        update.next = pending.next;//Add to the end of the linked list
        pending.next = update;
      }
      sharedQueue.pending = update;
    }
    

  2. this.forceUpdate is the same as this.setState, except that the tag will be assigned ForceUpdate

    //ReactBaseClasses.js
    Component.prototype.forceUpdate = function(callback) {
      this.updater.enqueueForceUpdate(this, callback, 'forceUpdate');
    };
    
    
    //ReactFiberClassComponent.old.js
    enqueueForceUpdate(inst, callback) {
        const fiber = getInstance(inst);
        const eventTime = requestEventTime();
        const suspenseConfig = requestCurrentSuspenseConfig();
        const lane = requestUpdateLane(fiber, suspenseConfig);
    
        const update = createUpdate(eventTime, lane, suspenseConfig);
        
        //tag assignment ForceUpdate
        update.tag = ForceUpdate;
        
        if (callback !== undefined && callback !== null) {
          update.callback = callback;
        }
        
        enqueueUpdate(fiber, update);
        scheduleUpdateOnFiber(fiber, lane, eventTime);
    
      },
    };
    

    If ForceUpdate is marked, the component Update in the render phase will be judged according to checkHasForceUpdateAfterProcessing and checkShouldComponentUpdate. If the Update tag is ForceUpdate, checkHasForceUpdateAfterProcessing is true. When the component is PureComponent, checkShouldComponentUpdate will shallow compare state and props, So when you use this.forceUpdate, it will be updated

    //ReactFiberClassComponent.old.js
    const shouldUpdate =
      checkHasForceUpdateAfterProcessing() ||
      checkShouldComponentUpdate(
        workInProgress,
        ctor,
        oldProps,
        newProps,
        oldState,
        newState,
        nextContext,
      );
    

    Overall process of status update

Update&updateQueue

After the update is triggered by HostRoot or ClassComponent, an update will be created in the function createUpdate, and the update will be calculated in beginWork in the later render stage. The update corresponding to functional component is described in Chapter 11. It is somewhat different from the update structure of HostRoot or ClassComponent

//ReactUpdateQueue.old.js
export function createUpdate(eventTime: number, lane: Lane): Update<*> {//Create update
  const update: Update<*> = {
    eventTime,
    lane,

    tag: UpdateState,
    payload: null,
    callback: null,

    next: null,
  };
  return update;
}

We mainly focus on these parameters:

  • lane: priority (Chapter 12)

  • tag: type of update, such as UpdateState and ReplaceState

  • Payload: the payload of ClassComponent is the first parameter of setState, and the payload of HostRoot is the first parameter of ReactDOM.render

  • callback: second parameter of setState

  • Next: connect the next Update to form a linked list. For example, when multiple setstates are triggered at the same time, multiple updates will be formed, and then connect with next

For HostRoot or ClassComponent, an updateQueue will be created using initializeUpdateQueue during mount, and then the updateQueue will be mounted on the fiber node

//ReactUpdateQueue.old.js
export function initializeUpdateQueue<State>(fiber: Fiber): void {
  const queue: UpdateQueue<State> = {
    baseState: fiber.memoizedState,
    firstBaseUpdate: null,
    lastBaseUpdate: null,
  shared: {
      pending: null,
    },
    effects: null,
  };
fiber.updateQueue = queue;
}
  • baseState: initial state. Based on this state, a new state will be calculated according to Update
  • firstBaseUpdate and lastBaseUpdate: the head and tail of the linked list formed by Update
  • Shared.pending: the newly generated update will be saved on shared.pending as a one-way circular list. When calculating the state, the circular list will be cut off and linked after lastBaseUpdate
  • effects: update whose callback is not null

Traverse upward from the fiber node that triggered the update to rootFiber

In the markUpdateLaneFromFiberToRoot function, the node triggering the update will be traversed upward to rootFiber. The traversal process will deal with the priority of the node (described in Chapter 15)

//ReactFiberWorkLoop.old.js
function markUpdateLaneFromFiberToRoot(
    sourceFiber: Fiber,
    lane: Lane,
  ): FiberRoot | null {
    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) {//Start from the node triggering the update and traverse upward to rootFiber
      parent.childLanes = mergeLanes(parent.childLanes, lane);//Merge childLanes priority
      alternate = parent.alternate;
      if (alternate !== null) {
        alternate.childLanes = mergeLanes(alternate.childLanes, lane);
      } else {
      }
      node = parent;
      parent = parent.return;
    }
    if (node.tag === HostRoot) {
      const root: FiberRoot = node.stateNode;
      return root;
    } else {
      return null;
    }
  }

For example, node B triggers an update. Node B is marked as normal update, that is, u1 in the figure, and then traverses upward to the root node, marking a normal update on the root node. If node B triggers another userBlocking update, it will also traverse upward to the root node, marking a userBlocking update on the root node.

If the update priority of the current root node is normal, both u1 and u2 participate in the status calculation. If the update priority of the current root node is userBlocking, only u2 participates in the calculation

dispatch

In ensureroot isscheduled, scheduleCallback will schedule the start function performSyncWorkOnRoot or performcurrentworkonroot of the render phase with a priority

//ReactFiberWorkLoop.old.js
if (newCallbackPriority === SyncLanePriority) {
  // The task has expired and the render phase needs to be executed synchronously
  newCallbackNode = scheduleSyncCallback(
    performSyncWorkOnRoot.bind(null, root)
  );
} else {
  // The render phase is executed asynchronously according to the task priority
  var schedulerPriorityLevel = lanePriorityToSchedulerPriority(
    newCallbackPriority
  );
  newCallbackNode = scheduleCallback(
    schedulerPriorityLevel,
    performConcurrentWorkOnRoot.bind(null, root)
  );
}

Status update

The classComponent status calculation occurs in the processUpdateQueue function and involves many linked list operations. It is more straightforward to see the figure

  • Initially, there are firstBaseUpdate (update1) and lastBaseUpdate (update2) on the fiber.updateQueue single chain table, which are connected by next

  • fiber.updateQueue.shared there are update3 and update4 in the circular linked list, which are connected with each other by next connection

  • When calculating the state, first cut the fiber.updateQueue.shared circular linked list to form a single linked list, and connect it behind fiber.updateQueue to form baseUpdate

  • Then traverse the linked list and calculate the memoizedState according to the baseState

Status update with priority

Similar to git submission, c3 here means high priority tasks, such as user departure events, data requests, synchronously executed code, etc.

  • The application created through ReactDOM.render does not have the concept of priority. Compared with git submission, it is equivalent to commit ting first and then submitting c3

  • In the concurrent mode, similar to git rebase, the previous code is temporarily stored, developed on the master, and then rebased to the previous branch

    The priority is scheduled by the Scheduler. Here, we only care about the priority ranking during status calculation, that is, the calculation in the function processUpdateQueue. For example, there are four updates c1-c4 at the beginning, of which c1 and c3 are high priority

    1. In the first render, low priority update s will be skipped, so only c1 and c3 are added to the state calculation
    2. In the second render, the update (c1) before the skipped update (c2) in the first will be used as the baseState, and the skipped update and subsequent updates (c2, c3, c4) will be recalculated as the baseUpdate

    In concurrent mode, componentWillMount may execute multiple times, which is inconsistent with the previous version

    Note that fiber.updateQueue.shared exists in workInprogress Fiber and current Fiber at the same time. The purpose is to prevent state loss caused by high priority interrupting the ongoing calculation. This code also occurs in processUpdateQueue

Look at the demo_ Priority of 8

Now let's look at the function that calculates the state

//ReactUpdateQueue.old.js
export function processUpdateQueue<State>(
  workInProgress: Fiber,
  props: any,
  instance: any,
  renderLanes: Lanes,
): void {
  const queue: UpdateQueue<State> = (workInProgress.updateQueue: any);
  hasForceUpdate = false;

  let firstBaseUpdate = queue.firstBaseUpdate;//First Update of updateQueue
  let lastBaseUpdate = queue.lastBaseUpdate;//Last Update of updateQueue
  let pendingQueue = queue.shared.pending;//Uncomputed pendingQueue

  if (pendingQueue !== null) {
    queue.shared.pending = null;
    const lastPendingUpdate = pendingQueue;//The last update of an uncomputed ppendingQueue
    const firstPendingUpdate = lastPendingUpdate.next;//The first update of an uncomputed pendingQueue
    lastPendingUpdate.next = null;//Cut open the circular linked list
    if (lastBaseUpdate === null) {//Add pendingQueue to updateQueue
      firstBaseUpdate = firstPendingUpdate;
    } else {
      lastBaseUpdate.next = firstPendingUpdate;
    }
    lastBaseUpdate = lastPendingUpdate;

    const current = workInProgress.alternate;//Do the same on current
    if (current !== null) {
      const currentQueue: UpdateQueue<State> = (current.updateQueue: any);
      const currentLastBaseUpdate = currentQueue.lastBaseUpdate;
      if (currentLastBaseUpdate !== lastBaseUpdate) {
        if (currentLastBaseUpdate === null) {
          currentQueue.firstBaseUpdate = firstPendingUpdate;
        } else {
          currentLastBaseUpdate.next = firstPendingUpdate;
        }
        currentQueue.lastBaseUpdate = lastPendingUpdate;
      }
    }
  }

  if (firstBaseUpdate !== null) {
    let newState = queue.baseState;

    let newLanes = NoLanes;

    let newBaseState = null;
    let newFirstBaseUpdate = null;
    let newLastBaseUpdate = null;

    let update = firstBaseUpdate;
    do {
      const updateLane = update.lane;
      const updateEventTime = update.eventTime;
      if (!isSubsetOfLanes(renderLanes, updateLane)) {//Determine whether priority is enough
        const clone: Update<State> = {//The priority is not enough. Skip the current update
          eventTime: updateEventTime,
          lane: updateLane,

          tag: update.tag,
          payload: update.payload,
          callback: update.callback,

          next: null,
        };
        if (newLastBaseUpdate === null) {//Save skipped update
          newFirstBaseUpdate = newLastBaseUpdate = clone;
          newBaseState = newState;
        } else {
          newLastBaseUpdate = newLastBaseUpdate.next = clone;
        }
        newLanes = mergeLanes(newLanes, updateLane);
      } else {
        //It will not be calculated until newLastBaseUpdate is null, so as to prevent the updateQueue from not completing the calculation
        if (newLastBaseUpdate !== null) {
          const clone: Update<State> = {
            eventTime: updateEventTime,
            lane: NoLane,

            tag: update.tag,
            payload: update.payload,
            callback: update.callback,

            next: null,
          };
          newLastBaseUpdate = newLastBaseUpdate.next = clone;
        }

        newState = getStateFromUpdate(//Calculate state based on updateQueue
          workInProgress,
          queue,
          update,
          newState,
          props,
          instance,
        );
        const callback = update.callback;
        if (callback !== null) {
          workInProgress.flags |= Callback;//Callback flag
          const effects = queue.effects;
          if (effects === null) {
            queue.effects = [update];
          } else {
            effects.push(update);
          }
        }
      }
      update = update.next;//Next update
      if (update === null) {//Reset updateQueue
        pendingQueue = queue.shared.pending;
        if (pendingQueue === null) {
          break;
        } else {
          const lastPendingUpdate = pendingQueue;

          const firstPendingUpdate = ((lastPendingUpdate.next: any): Update<State>);
          lastPendingUpdate.next = null;
          update = firstPendingUpdate;
          queue.lastBaseUpdate = lastPendingUpdate;
          queue.shared.pending = null;
        }
      }
    } while (true);

    if (newLastBaseUpdate === null) {
      newBaseState = newState;
    }

    queue.baseState = ((newBaseState: any): State);//New state
    queue.firstBaseUpdate = newFirstBaseUpdate;//New first update
    queue.lastBaseUpdate = newLastBaseUpdate;//New last update

    markSkippedUpdateLanes(newLanes);
    workInProgress.lanes = newLanes;
    workInProgress.memoizedState = newState;
  }

	//...
}

Posted by bw on Thu, 02 Dec 2021 17:01:06 -0800