SoFunction
Updated on 2025-03-10

React source code state calculation process and priority instance analysis

What happens after setState is executed

setStateAfter execution, aenqueueSetStateThe main function of this method is to createUpdateObject and initiation schedule, you can see the logic of this function.

enqueueSetState: function (inst, payload, callback) {
    // 1. inst is a component instance, and get the current component's Fiber node from the component instance    var fiber = get(inst);
    var eventTime = requestEventTime();
    var lane = requestUpdateLane(fiber);
    // 2.1 Create an update object based on the update initiation time, priority, and updated payload    var update = createUpdate(eventTime, lane);
     = payload;
    // 2.2 If setState has a callback, assign the callback to the callback property of the update object    if (callback !== undefined && callback !== null) {
       = callback;
    }
    // 3. Associate the update object into the updateQueue property of the Fiber node    enqueueUpdate(fiber, update);
    // 4. Initiate scheduling    var root = scheduleUpdateOnFiber(fiber, lane, eventTime);
}

From the above source code, you can clearly know.setState4 things to do after calling

  • Get its Fiber node based on component instance
  • createUpdateObject
  • WillUpdateThe object is associated with the Fiber nodeupdateQueueIn the properties
  • Initiate a schedule

Get its Fiber node based on component instance

Actually, it's the component instance_reactInternalsAttribute, this is the Fiber node corresponding to the current component

function get(key) {
  return key._reactInternals;
}

Off topic: react uses the double cache mechanism to complete the construction and replacement of Fiber trees, that is,currentandworkInProgressTwo trees, thatenqueueSetStateWhat is the Fiber node under the tree inside?

The answer is: the Fiber node under the current tree. The specific principle is explained below.

Create update object

function createUpdate(eventTime, lane) {
  var update = {
    eventTime: eventTime,
    lane: lane,
    tag: UpdateState,
    payload: null,
    callback: null,
    next: null
  };
  return update;
}

The meaning of attributes is as follows:

  • eventTime: The time when the update object was created, used forensureRootIsScheduledCalculate expiration time
  • lane: Priority for this update
  • payload: the first parameter of setState
  • callback: The second parameter of setState
  • next: The next update object to the connection

Associate the Update object to the updateQueue property of the Fiber node

What is executed here isenqueueUpdateFunction, below is my simplified logic

function enqueueUpdate(fiber, update) {
    var updateQueue = ;
    var sharedQueue = ;
    var pending = ;
    if (pending === null) {
       = update;
    } else {
       = ;
       = update;
    }
     = update;
}

You can see that the logic here is mainly to place the update object into the fiber object.In the attribute,It is a circular link list.

Then why do you need to design it as a circular linked list? That's how I understand

  • The last node of the linked list is stored. In the ring-shaped link list, the next pointer of the last node of the linked list points to the head node of the ring-shaped link list, so that we can quickly know the head and tail nodes of the linked list.
  • When you know the head and tail nodes, you can easily merge two linked lists. For example, there are two linked lists a and b. If we want to append b to the back of a, we can do this.
const lastBPoint = bTail
const firstBPoint = 
 = null
 = firstBPoint
aTail = lastBPoint

Even if there are c and d links in the following, you can also merge them into a in the same way. react in buildingupdateQueueSimilar techniques were used on the linked list, and the newly generatedupdateObjects are merged intoupdateQueueLink list,

Initiate a schedule

existenqueueUpdateAt the end, executedscheduleUpdateOnFiberfunction, the method will eventually be calledensureRootIsScheduledFunctions to schedule react's application root node.

When enteringperformConcurrentWorkOnRootWhen the function is enteredreconcileStage, which is what we sayrenderstage.renderThe stage is a process from top to bottom to top, traversing downward from react's application root node, and then returning from the bottom node to top. This isrenderThe node traversal process of stage.

What we need to know here is thatrenderDuring the top-down traversal of the stage, if we encounter a component-type Fiber node, we will execute it.processUpdateQueueFunction, this function is mainly responsible for the calculation of state when component updates

What did processUpdateQueue do

processUpdateQueueThe function mainly does three things

  • Construct this round of updatesupdateQueueand cache it in the currentFiber node
  • Loop traversalupdateQueue, calculatednewState, construct the next round of updatesupdateQueue
  • Update the workInProgress nodeupdateQueuememoizedStateproperty

HereupdateQueueDoes not refer to the Fiber node in the source codeupdateQueue, can be understood asfirstBaseUpdatearrivelastBaseUpdatethe entire update queue. For the convenience of description and understanding, use it directlyupdateQueueAlternative description.

Variable explanation

Because there are many variables involved,processUpdateQueueThe logic of the function does not look very clear, so I will first list some variable explanations for easy understanding.

enqueueSetStateThe generated update object ring link list

  • first/lastBaseUpdate:-- Next I will use baseUpdate instead

In the current Fiber nodeupdateQueueThe attributes in the object represent the head and tail nodes of the entire update queue list of the current component

  • first/lastPendingUpdate:Next I will use pendingUpdate instead

The products cut out represent the beginning and end nodes of the newly generated update object linked list, and will eventually be merged into the end of the update queue of the currentFiber and workInProgress trees.

  • newFirst/LastBaseUpdate:Next I will use newBaseUpdate instead

The newState calculation process will obtain that as long as there is a low priority update object, these two variables will have values. These two variables will be assigned to the workInProgressbaseUpdate, as the head and tail node of the linked list in the next round of update object

  • baseState: newState The initial state that the calculation process depends on
  • memoizedState: the state of the current component instance,processUpdateQueueThe newState will be assigned to this variable at the end.

Construct the updateQueue for this round of update

We mentioned aboveyesenqueueSetStateThe generated update object ring-link list, here we need to cut off this ring list to obtain the head and tail nodes in it, and form our update queue. So how to cut it?

It is the tail node of the ring-linked list, and its next node is the head node of the ring-linked list. Refer to the linked list merge operation we mentioned in the previous section.

var lastPendingUpdate = ;
var firstPendingUpdate = ;
 = null;

This will cut the circular link list and get the new update object we want—pendingUpdate. Then we're going to take thispendingUpdateDo two things:

  • WillpendingUpdateUpdate queue merged to the current Fiber node
  • WillpendingUpdateMerge into the update queue corresponding to the Fiber node in the currentFiber tree

Why do these two things?

  • The first is to solve the state continuity problem. When multiple setState updates occur, we must ensure that the update of the current update object is the state calculated by the previous update object. So we need to construct an update queue, and the new update object is to be merged into the tail of the update queue, thereby maintaining the continuity of the state calculation.
  • The second is to solve the problem of update object loss. existAfter being cut,It will be assigned a value of null. When a high priority task comes in, the low priority task will be interrupted, which means that the workInProgress tree will be restored.Get it after cutting itpendingUpdateWill be lost. At this time, you need topendingUpdateMerged into the update queue of the currentFiber tree

Next, you can take a look at the source code of this part

  var queue = ;
  var firstBaseUpdate = ;
  var lastBaseUpdate = ;
  // 1. Get the update object ring link list that was updated this time first  var pendingQueue = ;
  if (pendingQueue !== null) {
    // 2. Clear pending     = null;
    var lastPendingUpdate = pendingQueue;
    var firstPendingUpdate = ;
    // 3. Cut the ring link list     = null;
    // 4. Merge pendingupdate into baseUpdate    if (lastBaseUpdate === null) {
      firstBaseUpdate = firstPendingUpdate;
    } else {
       = firstPendingUpdate;
    }
    lastBaseUpdate = lastPendingUpdate;
    // 5. Merge pendingupdate into the baseUpdate of the currentFiber tree    var current = ;
    if (current !== null) {
      var currentQueue = ;
      var currentLastBaseUpdate = ;
      if (currentLastBaseUpdate !== lastBaseUpdate) {
        if (currentLastBaseUpdate === null) {
           = firstPendingUpdate;
        } else {
           = firstPendingUpdate;
        }
         = lastPendingUpdate;
      }
    }
  }

The source code looks a lot, but it only does one thing in essence. From the source code, it can be seen that this part is mainly to put theCut it open and get ourspendingUpdate, thenpendingUpdateMerged into this round of updates and currentFiber nodesbaseUpdatemiddle.

calculatenewState

In this part of the source code, except for calculationnewState, Another important task is to construct the next round of updatesupdateQueue

Here you may have questions, why you need to construct the next round of updatesupdateQueue, we will update this round ofAfter traversing the objects inside, the state is updated, and then the next round of updates is in, and then the calculation based on this state is not OK?

If there is no high priority task interruption mechanism, there is indeed no need to construct the next round of updates here.updateQueue, because every round of updates we will only rely on the current state and

Under the interrupt mechanism, the execution of low-priority tasks after restarting requires a complete update queue to ensure the continuity and correctness of state. Let me give you an example

state = { count: 0 }
componentDidMount() {
    const button = 
    // Low priority tasks    setTimeout(() => ({ count: 1 }), 1000)
    // High priority tasks    setTimeout(() => (), 1040)
}
handleButtonClick = () => {
    ( prevState => {
      return { count:  + 2 }
    } )
}

What we expect to achieve is0 -> 2 -> 3, the requirements are as follows:

  • After the high priority task interrupts the low priority task, the baseState calculated by the low priority task is not calculated.
  • After the low-priority task is restarted, the value calculated by the high-priority task cannot be overwritten, and the newState calculated by the low-priority task needs to be executed as the high-priority baseState.

Once we know the requirements, we can roughly list the implementation ideas:

  • After the low priority task is interrupted, before the high priority task is executed, it is necessary to restore to the workInPregress node before the low priority task is executed to ensure that it is not affected by the baseState calculated by the low priority task.
  • A queue of update objects needs to be maintained, and the update objects are stored in the order of execution, ensuring that high priority tasks will still be performed after a low priority restart.

The requirements and implementation ideas mentioned above are actually very simple to implement in react's source code, but it may take some effort to understand the meaning. Let's take a look at the source code I changed. You can directly fromdo...whileStart watching

  function cloneUpdate(update) {
      return {
          eventTime: ,
          lane: ,
          tag: ,
          payload: ,
          callback: ,
          next: null
      };
  }
  if (firstBaseUpdate !== null) {
    var newState = ;
    var newBaseState = null;
    var newFirstBaseUpdate = null;
    var newLastBaseUpdate = null;
    var update = firstBaseUpdate;
    // traversal updateQueue    do {
      var updateLane = ;
      var updateEventTime = ;
      // Verify whether the current update object is prioritized enough      if (!isSubsetOfLanes(renderLanes, updateLane)) {
        // The priority is not enough, we need to reconstruct an update queue from the current update object        var clone = cloneUpdate(update)
        if (newLastBaseUpdate === null) {
          newFirstBaseUpdate = newLastBaseUpdate = clone;
          // The current newState is used as the baseState for the next round of updates          newBaseState = newState;
        } else {
          newLastBaseUpdate =  = clone;
        }
      } else {
        // Enough priority        if (newLastBaseUpdate !== null) {
          // NewLastBaseUpdate is not empty, which means there is an update object with insufficient priority.          var _clone = cloneUpdate(update)
          // To ensure state continuity, even if the current update object has sufficient priority, it must be placed in updateQueue          newLastBaseUpdate =  = _clone;
        }
        // Calculate newState        newState = getStateFromUpdate(workInProgress, queue, update, newState, props, instance);
      }
      update = ;
    } while (update);

The logic is as follows:

Not enough priority

  • Reconstruct the update queuenewBaseUpdate, save it until the low priority task restart traversal
  • Record the currentnewState, leave it to the low priority task restart as baseState calculation

Enough priority

  • have a looknewBaseUpdateIf there is something, merge the current update object into it.
  • calculatenewState

herenewStateThe calculation logic is very simple

  • payload is a value. Just merge with object packages and merge them into prevState
  • payload is a function. Pass in prevState calculation, and merge the return value of the function into prevState

Update workInProgress node

There is not much logic to update the workInProgress node properties, mainlynewBaseState、newBaseUpateAssign to the workInProgress node as the next round of updatesbaseStateand update queue use

if (newLastBaseUpdate === null) {
  newBaseState = newState;
}
 = newBaseState;
 = newFirstBaseUpdate;
 = newLastBaseUpdate;
 = newState;
  • ifnewLastBaseUpdateis empty, which means that all update objects are empty, and the calculation obtained by this round of updatesnewStateCan be completely updated as the next roundbaseStateuse. Otherwise, you can only use the cached update object that is not prioritized.newStateAs the next round of updatebaseState
  • renewbaseUpdate, when all update objects have priority,baseUpdateThe value of u is generally empty. Values ​​will only occur if there is an update object with insufficient priority.
  • WillnewStateAssign tomemoizedStatememoizedStateRepresents all states of the current component

Summarize

Seeing whether the above principle analysis is very complicated, we can ignore all implementation details and return to the essence of the phenomenon.State calculation is to traverse the update object. The linked list obtains a new state based on payload.. Under this premise, due to the priority mechanism, it will be restored after interruption.workInProgressnode, which can cause update object loss problem and state calculation continuity problem. Solving these two problems is the complex implementation details we mentioned above

Update object loss problem

Why is it lost

We know that high priority tasks will interrupt the execution of low priority tasks, and after interrupting, the currentworkInProgressThe node is restored to the starting state, which can be understood asworkInProgressThe tree is restored to the rendered on the current pagecurrentFibernode. whenworkInProgressAfter the node is restored, we originally existedworkInProgressIn-houseupdateQueueThe properties will also be reset, which means that the low priority update object will be lost.

As mentioned above, the new update object generated by setState will be placedcurrentFiberThis is also the reason on the node. If the new update object generated by setState is placed inworkInProgressUp, as long asworkInProgressIf restored, these update objects will be lost

How to solve it

We're inprocessUpdateQueueThe function starts with the newly generated update object, that is,The values ​​incurrentFiber( )Node'sfirstBaseUpdateandlastBaseUpdate. The specific rules are as follows

  • currentFiberThe node does not existlastBaseUpdate, assign the new update object tocurrentFiberNode'sfirstBaseUpdateandlastBaseUpdateproperty
  • currentFiberNode existslastBaseUpdate, splice the new update object tocurrentFiberNode'slastBaseUpdateBehind the node, that is, the new update object will becomecurrentFiberNode newlastBaseUpdatnode

reductionworkInProgressThe function executed by the node isprepareFreshStack, can be used insidecurrentFiberProperties override of nodesworkInProgressnode, thereby realizing the restore function. So evenworkInProgressThe node is reset, we just need to merge the update object intocurrentFiberOn the node, it will still exist in the new one when restoringworkInProgressnode

Continuity of state calculation

Problem phenomenon

As mentioned above, the restart of the low-priority task cannot cover the value calculated by the high-priority task, and the newState calculated by the low-priority task needs to be executed as the high-priority baseState. What does this mean?

state = { count: 0 }
componentDidMount() {
    const button = 
    // Low priority tasks - AUpate    setTimeout(() => ({ count: 1 }), 1000)
    // High priority tasks - BUpdate    setTimeout(() => (), 1040)
}
handleButtonClick = () => {
    ( prevState => {
      return { count:  + 2 }
    } )
}

The update object generated by the above code is as follows

AUpate = { lane: Low, payload: 1 }
BUpdate = { lane: high, payload: state => ({ count:  + 2 }) }
  • Execute firstAUpdateTask
  • AUpdatePriority ratioBUpdateLow,BUpdateWill be interruptedAUpdateexecution.
  • SoBUpdateAfter execution, the value of count is 2. The problem is here
  • BUpdateIt's coming in later,AUpdateCan't be coveredBUpdateResults
  • AUpdateThe result of execution count will become 1, thenBUpdateThe result needs to be calculated on this basis, that is, you need to obtain 3

This also determines that we need to store all update objects in the form of a queue. The storage order of the update object determines the dependence of state calculations, thereby ensuring the continuity and accuracy of state.

clearA very important point, priority will only affect whether a certain update object will be executed in advance and will not affect the final state result. The final state result is still determined by the order of update objects in the update queue.

How to solve it

We seeprocessUpdateQueueTwo parts are constructing the update queue

  • Part of it is at the beginning of the function, merge the update object intocurrentFibernode
  • Some are located at the end of the function,newBaseUpdateAssign toworkInProgressNode These two parts combine to perfectly solve our needs.currentFiberIt is used for this round of updates.workInProgressThis will be used for the next round of updates. Because of the existence of the double cache mechanism, at the end of the commit phase, the current pointer of the react application root node will point toworkInProgressnode,workInProgressThe node will become a new update in the next round ofcurrentFibernode.

In this way, no matter what priority, as long as the update queue is constructed in order, I can calculate the correct newState, and at the same time, using the properties of the queue to ensure the continuity of state calculations between update objects

The above is the detailed content of the React source code state calculation process and priority instance analysis. For more information on the priority of the React state calculation process, please pay attention to my other related articles!