One, foreword
React is a popular front-end framework. If you’re a React developer, there’s an 80% or more chance you’ll be asked about setState when you go out for an interview. For example, setState is synchronous or asynchronous, setState batch update how to achieve and so on. A lot of you might say that setState is an asynchronous update, but it’s not. So what’s the real answer? Next, analyze setState from the source code to understand it fundamentally.
SetState in Legacy mode
Lagacy mode is the default mode currently used by reactDom, the application created by reactdom.render. It differs from concurrent mode in the priority of updates, while Lagacy mode has syncLane, or synchronization priority. Concurrent has different priorities for different scenarios. Why is priority mentioned here? Because reactDom has different logic for handling setState at different priorities. Calling setState eventually calls scheduleUpdateOnFiber to schedule an update, as shown in the code below
if (lane === SyncLane) { if ( // executionContext & LegacyUnbatchedContext ! NoContext (executionContext & LegacyUnbatchedContext)! == NoContext && // Check if we're not already rendering (executionContext & (RenderContext | CommitContext)) === NoContext) { schedulePendingInteractions(root, lane); // Synchronize schedule update performSyncWorkOnRoot(root); } else {// Asynchronous scheduling update ensureRootIsScheduled(root, eventTime); schedulePendingInteractions(root, lane); if (executionContext === NoContext) { resetRenderTimer(); flushSyncCallbackQueue(); } } } else { // ... }Copy the code
As we said above, the application created by reactdom.render will have a SyncLane lane, which will enter the logic of if with an if else, And the key to this if else variable is whether or not the executionContext variable contains batch updates. If not, performSyncWorkOnRoot will be called synchronously to start the update. Otherwise, the ensureRootIsScheduled call is updated in order. So where does executionContext get assigned? Take the example of a click click event calling setState, which will eventually be called
batchedEventUpdates(function () {
return dispatchEventsForPlugins(domEventName, eventSystemFlags, nativeEvent, ancestorInst);
});
Copy the code
And this batchedEventUpdates is going to call batchedEventUpdates$1
function batchedEventUpdates$1(fn, a) { var prevExecutionContext = executionContext; executionContext |= EventContext; try { return fn(a); } finally { executionContext = prevExecutionContext; if (executionContext === NoContext) { resetRenderTimer(); flushSyncCallbackQueue(); }}}Copy the code
You can see that batchedEventUpdates$1 will execute Context bitbit or EventContext, and that EventContext will not contain LegacyUnbatchedContext, This fn is the ensureRootIsScheduled asynchronous update logic that is ensureRootIsScheduled, so the setState is asynchronous in this case. Is that the right answer? Obviously not. Here’s an example
setTimeout(() => {
this.setState({...})
})
Copy the code
When we call setState with setTimeout, the call stack for this.setState() is not in batchedEventUpdates1, so we don’t get batchedEventUpdates1. I’m not going to get batchedEventUpdates1, I’m not going to get executionContext in the batchedEventUpdates1 call stack, so I’m going to go to performSyncWorkOnRoot and schedule updates, So when we call setState in setTimeout we immediately get the updated state. So the setState in this case is synchronous. At this point, do you think that’s the right answer? Obviously not. There is also concurrent mode.
SetState in concurrent mode
Concurrent’s lane is not SyncLane, so it goes into scheduleUpdateOnFiber’s else logic
if (lane === SyncLane) {
// ...
} else {
// ...
ensureRootIsScheduled(root, eventTime);
schedulePendingInteractions(root, lane);
}
Copy the code
We can see that setState in concurrent mode is always scheduled asynchronously, that is, even if setState is called from setTimeout, it is asynchronous. Now you already know the answer to the question, “is setState synchronous or asynchronous?”
Iv. How to achieve batch update of setState
During the interview process, one of the most common questions asked is, how many times will setState be updated in a function? Take this example
setCount() {
this.setState({count: 1})
this.setState({count: 2})
this.setState({count: 3})
}
Copy the code
Many of you know the answer, once. So why once? React does batch updates for this situation. Here’s how react implements batch updates. EnsureRootIsScheduled is one ensureRootIsScheduled call each time setState is called. EnsureRootIsScheduled is one of the ensureRootIsScheduled calls. Let’s see how ensureRootIsScheduled is handled
function ensureRootIsScheduled(root, currentTime) { var existingCallbackNode = root.callbackNode; / /... Var newCallbackPriority = returnNextLanesPriority(); / /... if (existingCallbackNode ! Var existingCallbackPriority = root.callbackPriority; if (existingCallbackPriority === newCallbackPriority) { return; } cancelCallback(existingCallbackNode); } var newCallbackNode; // Functions that start with scheduler are scheduler related apis that register tasks with different priorities to wait for asynchronous calls. And return the task if (newCallbackPriority === SyncLanePriority) {newCallbackNode = scheduleSyncCallback(performSyncWorkOnRoot.bind(null, root)); } else if (newCallbackPriority === SyncBatchedLanePriority) { newCallbackNode = scheduleCallback(ImmediatePriority$1, performSyncWorkOnRoot.bind(null, root)); } else { var schedulerPriorityLevel = lanePriorityToSchedulerPriority(newCallbackPriority); newCallbackNode = scheduleCallback(schedulerPriorityLevel, performConcurrentWorkOnRoot.bind(null, root)); } root.callbackPriority = newCallbackPriority; root.callbackNode = newCallbackNode; }Copy the code
This code is very clever, so let’s look at it
- The first setState, where existingCallbackNode is null and newCallbackPriority is SyncLanePriority, scheduleSyncCallback will be used to schedule an update
- The task returned by scheduleSyncCallback is assigned to root.callbackNode, and the setState priority is assigned to root.callbackPriority. Root is the root node of the Fiber tree
- The second setState, in which existingCallbackNode is the task of the previous setState, enters existingCallbackNode! == null; if you enter existingCallbackPriority === newCallbackPriority, return it without registering the update task with the scheduler again. So how do you guarantee that setState will be the same priority multiple times?
When we call setState, we call enqueueSetState, which calls requestUpdateLane to get a priority for the update. React must handle the priority in this function
function requestUpdateLane(fiber) { var mode = fiber.mode; if ((mode & BlockingMode) === NoMode) { return SyncLane; } else if ((mode & ConcurrentMode) === NoMode) { return getCurrentPriorityLevel() === ImmediatePriority$1 ? SyncLane : SyncBatchedLane; } / /... var schedulerPriority = getCurrentPriorityLevel(); var lane; if ( (executionContext & DiscreteEventContext) ! == NoContext && schedulerPriority === UserBlockingPriority$2) { lane = findUpdateLane(InputDiscreteLanePriority, currentEventWipLanes); } else { var schedulerLanePriority = schedulerPriorityToLanePriority(schedulerPriority); lane = findUpdateLane(schedulerLanePriority, currentEventWipLanes); } return lane; }Copy the code
We can see the distinction between lagacy, blocking, and Concurrent modes
Lagacy mode
Lagacy mode returns SyncLane directly, which is relatively simple
if ((mode & BlockingMode) === NoMode) {
return SyncLane;
}
Copy the code
Blocking mode (which is an intermediate mode as a transition to concurrent mode)
Blocking returns SyncLane or SyncBatchedLane based on the call to getCurrentPriorityLevel, GetCurrentPriorityLevel obtains a React priority based on the priority of the current schedule. This time, the react priority is obtained in the same schedule
if ((mode & ConcurrentMode) === NoMode) {
return getCurrentPriorityLevel() === ImmediatePriority$1 ? SyncLane : SyncBatchedLane;
}
Copy the code
Concurrent mode
if (currentEventWipLanes === NoLanes) { currentEventWipLanes = workInProgressRootIncludedLanes; } var schedulerPriority = getCurrentPriorityLevel(); var lane; if ( (executionContext & DiscreteEventContext) ! == NoContext && schedulerPriority === UserBlockingPriority$2) { // ... } else { var schedulerLanePriority = schedulerPriorityToLanePriority(schedulerPriority); lane = findUpdateLane(schedulerLanePriority, currentEventWipLanes); } return lane;Copy the code
It can be seen that lane is finally obtained by combining schedulerLanePriority and currentEventWipLanes, so the goal can be achieved by ensuring that the two variables of the NTH setState and the first setState are the same.
- The schedulerLanePriority returns a React priority based on the execution priority of the current scheduler. The schedulerLanePriority is the same since it is in an execution session
- CurrentEventWipLanes, first setState would give currentEventWipLanes workInProgressRootIncludedLanes assignment, And this workInProgressRootIncludedLanes is last update lane, when the second setState, because there are the same upper call stack, so get to currentEventWipLanes, This ensures that the same lane is retrieved multiple times by setState
“EnsureRootIsScheduled” existingCallbackPriority === newCallbackPriority “can be one of the ensureRootIsScheduled modes with different processing logic to obtain the same lane multiple times. So as to achieve the purpose of batch update.