What happens after setState is executed
setState
After execution, aenqueueSetState
The main function of this method is to createUpdate
Object 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.setState
4 things to do after calling
- Get its Fiber node based on component instance
- create
Update
Object - Will
Update
The object is associated with the Fiber nodeupdateQueue
In the properties - Initiate a schedule
Get its Fiber node based on component instance
Actually, it's the component instance_reactInternals
Attribute, 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,current
andworkInProgress
Two trees, thatenqueueSetState
What 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 for
ensureRootIsScheduled
Calculate 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 isenqueueUpdate
Function, 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 buildingupdateQueue
Similar techniques were used on the linked list, and the newly generatedupdate
Objects are merged intoupdateQueue
Link list,
Initiate a schedule
existenqueueUpdate
At the end, executedscheduleUpdateOnFiber
function, the method will eventually be calledensureRootIsScheduled
Functions to schedule react's application root node.
When enteringperformConcurrentWorkOnRoot
When the function is enteredreconcile
Stage, which is what we sayrender
stage.render
The 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 isrender
The node traversal process of stage.
What we need to know here is thatrender
During the top-down traversal of the stage, if we encounter a component-type Fiber node, we will execute it.processUpdateQueue
Function, this function is mainly responsible for the calculation of state when component updates
What did processUpdateQueue do
processUpdateQueue
The function mainly does three things
- Construct this round of updates
updateQueue
and cache it in the currentFiber node - Loop traversal
updateQueue
, calculatednewState
, construct the next round of updatesupdateQueue
- Update the workInProgress node
updateQueue
、memoizedState
property
HereupdateQueue
Does not refer to the Fiber node in the source codeupdateQueue
, can be understood asfirstBaseUpdate
arrivelastBaseUpdate
the entire update queue. For the convenience of description and understanding, use it directlyupdateQueue
Alternative description.
Variable explanation
Because there are many variables involved,processUpdateQueue
The logic of the function does not look very clear, so I will first list some variable explanations for easy understanding.
- :
enqueueSetState
The generated update object ring link list
- first/lastBaseUpdate:-- Next I will use baseUpdate instead
In the current Fiber nodeupdateQueue
The 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,
processUpdateQueue
The newState will be assigned to this variable at the end.
Construct the updateQueue for this round of update
We mentioned aboveyes
enqueueSetState
The 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 thispendingUpdate
Do two things:
- Will
pendingUpdate
Update queue merged to the current Fiber node - Will
pendingUpdate
Merge 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. exist
After 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 it
pendingUpdate
Will be lost. At this time, you need topendingUpdate
Merged 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 ours
pendingUpdate
, thenpendingUpdate
Merged into this round of updates and currentFiber nodesbaseUpdate
middle.
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...while
Start 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 queue
newBaseUpdate
, save it until the low priority task restart traversal - Record the current
newState
, leave it to the low priority task restart as baseState calculation
Enough priority
- have a look
newBaseUpdate
If there is something, merge the current update object into it. - calculate
newState
herenewState
The 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、newBaseUpate
Assign to the workInProgress node as the next round of updatesbaseState
and update queue use
if (newLastBaseUpdate === null) { newBaseState = newState; } = newBaseState; = newFirstBaseUpdate; = newLastBaseUpdate; = newState;
- if
newLastBaseUpdate
is empty, which means that all update objects are empty, and the calculation obtained by this round of updatesnewState
Can be completely updated as the next roundbaseState
use. Otherwise, you can only use the cached update object that is not prioritized.newState
As the next round of updatebaseState
- renew
baseUpdate
, when all update objects have priority,baseUpdate
The value of u is generally empty. Values will only occur if there is an update object with insufficient priority. - Will
newState
Assign tomemoizedState
,memoizedState
Represents 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.workInProgress
node, 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 currentworkInProgress
The node is restored to the starting state, which can be understood asworkInProgress
The tree is restored to the rendered on the current pagecurrentFiber
node. whenworkInProgress
After the node is restored, we originally existedworkInProgress
In-houseupdateQueue
The 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 placedcurrentFiber
This is also the reason on the node. If the new update object generated by setState is placed inworkInProgress
Up, as long asworkInProgress
If restored, these update objects will be lost
How to solve it
We're inprocessUpdateQueue
The function starts with the newly generated update object, that is,The values in
currentFiber( )
Node'sfirstBaseUpdate
andlastBaseUpdate
. The specific rules are as follows
-
currentFiber
The node does not existlastBaseUpdate
, assign the new update object tocurrentFiber
Node'sfirstBaseUpdate
andlastBaseUpdate
property -
currentFiber
Node existslastBaseUpdate
, splice the new update object tocurrentFiber
Node'slastBaseUpdate
Behind the node, that is, the new update object will becomecurrentFiber
Node newlastBaseUpdat
node
reductionworkInProgress
The function executed by the node isprepareFreshStack
, can be used insidecurrentFiber
Properties override of nodesworkInProgress
node, thereby realizing the restore function. So evenworkInProgress
The node is reset, we just need to merge the update object intocurrentFiber
On the node, it will still exist in the new one when restoringworkInProgress
node
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 first
AUpdate
Task -
AUpdate
Priority ratioBUpdate
Low,BUpdate
Will be interruptedAUpdate
execution. - So
BUpdate
After execution, the value of count is 2. The problem is here -
BUpdate
It's coming in later,AUpdate
Can't be coveredBUpdate
Results -
AUpdate
The result of execution count will become 1, thenBUpdate
The 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 seeprocessUpdateQueue
Two parts are constructing the update queue
- Part of it is at the beginning of the function, merge the update object into
currentFiber
node - Some are located at the end of the function,
newBaseUpdate
Assign toworkInProgress
Node These two parts combine to perfectly solve our needs.currentFiber
It is used for this round of updates.workInProgress
This 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 toworkInProgress
node,workInProgress
The node will become a new update in the next round ofcurrentFiber
node.
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!