State Update overview
What are the actions in React that trigger status updates:
- ReactDOM. Render: HostRoot in Legacy mode
- New ReactDOMRoot(“).render: HostRoot in concurrent mode
- This.setstate: An update to the ClassComponent type
- ForceUpdate: An update of the ClassComponent type
- UseState: Update to the FunctionComponent type
- UseReducer: Update to the FunctionComponent type
What is behind these actions to ensure that React updates correctly when it is in Legacy or concurrent mode? Before we get into the feature, there are a few precursors.
The Update object
What is an Update object? In short, Update object is a data structure, which can be divided into two types, one for ClassComponent and HostRoot, and the other for FunctionCompnent.
// Update for ClassComponent and HostRoot
export type Update<State> = {
eventTime: number.lane: Lane,
tag: 0 | 1 | 2 | 3.payload: any.callback: (() = > mixed) | null.next: Update<State> | null};// FunctionComponent update
type Update<S, A> = {
lane: Lane,
action: A,
eagerReducer: ((S, A) = > S) | null.eagerState: S | null.next: Update<S, A>, priority? : ReactPriorityLevel, };Copy the code
In this chapter, we’ll focus on ClassComponent and HostRoot status updates, and ignore FunctionComponent updates for the time being (more on that in the next chapter when we introduce Hooks). So let’s take a look at what each field means:
- EventTime: indicates the task time
performance.now()
The number of milliseconds obtained. - Lane: Priority field, some of which may be covered later.
- Tag: Update type: UpdateState, ReplaceState, ForceUpdate, CaptureUpdate.
- Payload: Payloay is the first parameter of setState.
- Callback: the second argument to setState, which is called during the Commit phase
fiber.updateQueue.effects
In the - Next: The next update
So how do you save updates in React?
class App extends React.Component {
constructor() {
this.state = {
count: 1}}onclickHandler() {
this.setState((count) = > count + 1);
this.setState((count) = > count + 1);
}
render() {
return (
<>
<div onClick={()= > this.onclickHandler()}>{this.state.count}</div>
</>)}}Copy the code
In the chestnuts above, what kind of data structure is going to form in React?
We called setState twice in onclickHandler, so how many render times, the answer is 1 (since it is set in the onClick callback, it triggers the batch update in the React message).
// update1 --> indicates the Update created by the first call to setState
// update2 --> indicates the Update created by the second call to setState
// Eventually a circular list is formed
update1.next = update2;
update2.next = update2;
fiber.updateQueue.shared.pending = update2;
Copy the code
Create the Update will form a circular linked list, and mount to fiber. The updateQueue. Shared. Pending.
Let’s first look at the Update circular list creation function enqueueUpdate
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;
} else {
update.next = pending.next;
pending.next = update;
}
sharedQueue.pending = update;
}
Copy the code
EnqueueUpdate: enqueueUpdate: enqueueUpdate: enqueueUpdate: enqueueUpdate: enqueueUpdate: enqueueUpdate: enqueueUpdate
Let’s say we want to create four updates: A, B, C, and D.
The following pending for fiber. UpdateQueue. Pending
- Pending === NULL, a.ext = a&&
fiber.updateQueue.pending
A.ext === A, pending === A, pending.next === A. - Create B when executing
update.next = pending.next
When, that is, B.ext = A, forming A ring, when executedpending.next = update
, a.ext = B, and then executesharedQueue.pending = update
Pending === B Pending === B, b.ext === A, pending === B, pending.next === A. - Create C when executing
update.next = pending.next
When, i.e. C.ext = A, forming A ring, when executingpending.next = update
, i.e. B.ext = C, and then executesharedQueue.pending = update
Pending === C. Pending === B, b.ext === A, pending === B, pending.next === A. - Create D and repeat the process of creating C.
Pending finally refers to the last element in the circular list, while pending.next refers to the first element in the circular list.
Let’s look at the updateQueue type:
UpdateQueue object
type SharedQueue<State> = {
pending: Update<State> | null};export type UpdateQueue<State> = {
baseState: State,
firstBaseUpdate: Update<State> | null.lastBaseUpdate: Update<State> | null.shared: SharedQueue<State>,
effects: Array<Update<State>> | null};Copy the code
The UpdateQueue object holds all updates that need to be updated for the current fiber (not just those triggered by this Update).
- BaseState: The base values for this update, and
fiber.memoizedState
It may be equal (last update, all updates were done), or it may not be equal (last update, no updates were done). - FirstBaseUpdate: Start Update of the Update, if the last Update was not fully updated, at the beginning of this calculation
firstBaseUpdate
Don’t be empty. - LastBaseUpdate: Update the final Update, if the last Update was not fully updated, at the beginning of this calculation
lastBaseUpdate
Don’t be empty. - Share: Update all updates this time.
- Effects: setState The second argument.
Ok, let’s look at the processUpdateQueue function to calculate memoizedState from Update:
The following functions are more complex, I will write comments in detail, the specific process can be repeated to track the code
export function processUpdateQueue<State> (
workInProgress: Fiber,
props: any,
instance: any,
renderLanes: Lanes,
) :void {
// This is always non-null on a ClassComponent or HostRoot
// Get updateQueue for current fiber
const queue: UpdateQueue<State> = (workInProgress.updateQueue: any);
hasForceUpdate = false;
if (__DEV__) {
currentlyProcessingQueue = queue.shared;
}
// If the last update did not complete all updates, the current firstBaseUpdate is not null
let firstBaseUpdate = queue.firstBaseUpdate;
// Same as firstBaseUpdate above
let lastBaseUpdate = queue.lastBaseUpdate;
// Get the circular list of this update
let pendingQueue = queue.shared.pending;
// If there is an update
if(pendingQueue ! = =null) {
// Reset pending to prepare for the next update
queue.shared.pending = null;
Queue.shared. pending refers to the last element of the circular list
// pengding.next refers to the first element
const lastPendingUpdate = pendingQueue;
// Get the first element
const firstPendingUpdate = lastPendingUpdate.next;
// Cut off the ring list
lastPendingUpdate.next = null;
// If null, set the current list to firstBaseUpdate
if (lastBaseUpdate === null) {
firstBaseUpdate = firstPendingUpdate;
} else {
// If lastBaseUpdate is not empty, add the current list
lastBaseUpdate.next = firstPendingUpdate;
}
// Update the final list
lastBaseUpdate = lastPendingUpdate;
// Add the current linked list after the workinprogress. alternate linked list to prevent data loss after the interruption
// Remember that react updates start at root and only one workInProgress tree exists
const current = workInProgress.alternate;
if(current ! = =null) {
// This is always non-null on a ClassComponent or HostRoot
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; }}}// The fiber has been updated
if(firstBaseUpdate ! = =null) {
// Get the updated base
let newState = queue.baseState;
/ / priority
let newLanes = NoLanes;
// This variable is used to save the previously computed base when the first low priority is encountered
let newBaseState = null;
// Save the first low-priority update encountered when the priority is different
let newFirstBaseUpdate = null;
// // Indicates the last update that is not updated in different priorities
let newLastBaseUpdate = null;
let update = firstBaseUpdate;
do {
const updateLane = update.lane;
const updateEventTime = update.eventTime;
// Check whether the priority is within the current update priority. If not, skip the current updata
if(! isSubsetOfLanes(renderLanes, updateLane)) {// Priority is insufficient. Skip this update. If this is the first
// skipped update, the previous update/state is the new base
// update/state.
const clone: Update<State> = {
eventTime: updateEventTime,
lane: updateLane,
tag: update.tag,
payload: update.payload,
callback: update.callback,
next: null};if (newLastBaseUpdate === null) {
// Below the first update of this update
newFirstBaseUpdate = newLastBaseUpdate = clone;
// Save the base calculated earlier
newBaseState = newState;
} else {
newLastBaseUpdate = newLastBaseUpdate.next = clone;
}
// Update the remaining priority in the queue.
// Update the priority
newLanes = mergeLanes(newLanes, updateLane);
} else {
// This update does have sufficient priority.
// If there are skipped updates, all updates must be saved from the skipped update point
if(newLastBaseUpdate ! = =null) {
const clone: Update<State> = {
eventTime: updateEventTime,
// This update is going to be committed so we never want uncommit
// it. Using NoLane works because 0 is a subset of all bitmasks, so
// this will never be skipped by the check above.
lane: NoLane,
tag: update.tag,
payload: update.payload,
callback: update.callback,
next: null}; newLastBaseUpdate = newLastBaseUpdate.next = clone; }// Process this update.
/ / update the state
newState = getStateFromUpdate(
workInProgress,
queue,
update,
newState,
props,
instance,
);
If so, save the callback in effects and call it during the COMMIT phase
const callback = update.callback;
if(callback ! = =null) {
workInProgress.flags |= Callback;
const effects = queue.effects;
if (effects === null) {
queue.effects = [update];
} else{ effects.push(update); }}}// Next update to be updated
update = update.next;
// The update is complete
if (update === null) {
pendingQueue = queue.shared.pending;
if (pendingQueue === null) {
break;
} else {
// To handle the fact that an update was triggered during the update process. Non-recommended practice
// An update was scheduled from inside a reducer. Add the new
// pending updates to the end of the list and keep processing.
const lastPendingUpdate = pendingQueue;
// Intentionally unsound. Pending updates form a circular list, but we
// unravel them when transferring them to the base queue.
const firstPendingUpdate = ((lastPendingUpdate.next: any): Update<State>);
lastPendingUpdate.next = null;
update = firstPendingUpdate;
queue.lastBaseUpdate = lastPendingUpdate;
queue.shared.pending = null; }}}while (true);
// Indicates low-priority updates that have not been skipped
if (newLastBaseUpdate === null) {
newBaseState = newState;
}
/ / save baseState
queue.baseState = ((newBaseState: any): State);
queue.firstBaseUpdate = newFirstBaseUpdate;
queue.lastBaseUpdate = newLastBaseUpdate;
// Set the remaining expiration time to be whatever is remaining in the queue.
// This should be fine because the only two other things that contribute to
// expiration time are props and context. We're already in the middle of the
// begin phase by the time we start processing the queue, so we've already
// dealt with the props. Context in components that specify
// shouldComponentUpdate is tricky; but we'll have to account for
// that regardless.
markSkippedUpdateLanes(newLanes);
workInProgress.lanes = newLanes;
workInProgress.memoizedState = newState;
}
if (__DEV__) {
currentlyProcessingQueue = null; }}Copy the code
After reading the above code, I wonder if students will have two questions:
- React can be interrupted in the Render phase. After an interruption, React needs to rebuild the workInProgess tree from the root phase. Will the current update be lost?
- How do you guarantee state dependencies in React?
If we want to understand this problem, we need to deal with the priority in React, as well as the conversion between Lanes and scheduling priorities in React. In this chapter, we first think that these two priorities are equivalent (we will talk about the scheduling in React in detail later).
correctness
When React interrupts a low-priority task, it builds a new workInProgress from the Root, and the current workInProgress is discarded. How does React save the Update? We all know that React always maintains two trees, the current Tree and the workInprogress Tree. The current Tree will be saved until the commit, so you can save the Update in the current Tree.
The code is as follows:
if(current ! = =null) {
// This is always non-null on a ClassComponent or HostRoot
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; }}Copy the code
In a COMMIT, the workInProgress Tree is replaced with the current Tree to ensure that the update is correct
Update Status continuity
We all know that updates have priority (i.e., lane fields). If an update has a lower priority than the current update, it will be skipped. The full explanation is commented in the code.
Let’s take a look at the explanation given in the code comment:
// For example:
//
// Given a base state of '', and the following queue of updates
//
// A1 - B2 - C1 - D2
//
// where the number indicates the priority, and the update is applied to the
// previous state by appending a letter, React will process these updates as
// two separate renders, one per distinct priority level:
//
// First render, at priority 1:
// Base state: ''
// Updates: [A1, C1]
// Result state: 'AC'
//
// Second render, at priority 2:
// Base state: 'A' <- The base state does not include C1,
// because B2 was skipped.
// Updates: [B2, C1, D2] <- C1 was rebased on top of B2
// Result state: 'ABCD'
//
// Because we process updates in insertion order, and rebase high priority
// updates when preceding updates are skipped, the final result is deterministic
// regardless of priority. Intermediate state may vary according to system
// resources, but the final state is always the same.
Copy the code
Let me translate this comment:
For example, if we have four updates, A1, B2, C1, and D2, the lower the number, the higher the priority. A, B, C, and D indicate the updated content.
-
The first update has priority 1
base state: ”
Update: [A1, C1]
Fiber. MemoizedState: AC
In this update, the update of B2 will be skipped, and the final result AC will be calculated based on the base state, A1, C1, and the base state is ‘A’.
-
The second update has priority 2
base state: ‘A’
Update: [B2, C1, D2]
Fiber. MemoizedState: ABCD
Select * from ‘A’ where base state = ‘A’; select * from ‘A’ where base state = ‘A’; select * from ‘A’ where base state = ‘A’;
We can see from the chestnuts above that React does not guarantee that the intermediate state is correct, only that the final result is correct.
It makes sense, too, because it’s a lot like Git rebase. Much of the logic is the same
In the next chapter we’ll cover the complete process of react.render () and this.setState.