preface
This is probably the first problem most people encounter when they first learn React. For me, the tutorial only tells me that using setState directly is asynchronous, but in some asynchronous methods such as setTimeout, it is synchronous.
I was very confused at the time, although I wanted to see what was going on, but in order to get started with React as soon as possible, I decided to use the app first and save it for later source code learning.
The content of this article is to analyze whether setState is synchronous or asynchronous from the source level. Since the current projects are using hooks to maintain data state, especially in the case of classes, I’m not going to get too deep into the underlying rendering principles, just why setState sometimes behaves synchronously and sometimes asynchronously.
example
export default class App extends React.Component{
state = {
num: 0
}
add = () = > {
console.log(Before the 'add'.this.state.num)
this.setState({
num: this.state.num + 1
});
console.log(After the 'add'.this.state.num)
}
add3 = () = > {
console.log('before add3'.this.state.num)
this.setState({
num: this.state.num + 1
});
this.setState({
num: this.state.num + 1
});
this.setState({
num: this.state.num + 1
});
console.log('after add3'.this.state.num)
}
reduce = () = > {
setTimeout(() = > {
console.log('before the reduce'.this.state.num)
this.setState({
num: this.state.num - 1
});
console.log('after the reduce'.this.state.num)
},0);
}
render () {
return <div>
<button onClick={this.add}>Click on add 1</button>
<button onClick={this.add3}>Click add 3 times</button>
<button onClick={this.reduce}>I 1 point</button>
</div>}}Copy the code
Click the three buttons in sequence to see what the console prints.
The result, for developers with a bit of React experience, is simple.
In this example, setState is an asynchronous method. After execution, the data is not modified immediately, but will be changed at a later time. Multiple calls to setState will only execute the latest event. In an asynchronous method, it will have synchronous properties.
So let’s not jump to conclusions, let’s dive into the setState process.
Principle of asynchrony – Batch update
In order to optimize performance, one setState will not trigger a complete update process. In a synchronous code run, one setState will be executed each time. React will put it into a queue and merge the “accumulated” state results when the time is ripe. Finally, only one update process is performed for the latest state value. This process is called batch updating.
This way, even if we write bad code, such as a method that loops 100 times and calls setState each time, it won’t cause frequent re-render to cause the page to lag.
This principle explains the phenomenon of the first button and the second button above.
Principle of synchronization -setState workflow
The only question is, why can setTimeout change the execution order of setState from asynchronous to synchronous?
Let’s look at the source code of setState
ReactComponent.prototype.setState = function (partialState, callback) {
this.updater.enqueueSetState(this, partialState);
if (callback) {
this.updater.enqueueCallback(this, callback, 'setState'); }};Copy the code
Regardless of the callback, an enqueueSetState method is triggered.
enqueueSetState: function (publicInstance, partialState) {
// Get the corresponding component instance according to this
var internalInstance = getInternalInstanceReadyForUpdate(publicInstance, 'setState');
// This queue corresponds to the state array of a component instance
var queue = internalInstance._pendingStateQueue || (internalInstance._pendingStateQueue = []);
queue.push(partialState);
// enqueueUpdate is used to process the current component instance
enqueueUpdate(internalInstance);
}
Copy the code
This method is what we just said, putting the state changes in the queue. EnqueueUpdate is then used to process the component instances to be updated. Look again at the enqueueUpdate method.
function enqueueUpdate(component) {
ensureInjected();
// Note that this sentence is the key to the problem. IsBatchingUpdates identifies whether you are currently in the batch creation/update phase of components
if(! batchingStrategy.isBatchingUpdates) {// Update components immediately if they are not currently in the batch creation/update phase
batchingStrategy.batchedUpdates(enqueueUpdate, component);
return;
}
// Otherwise, put the component in the dirtyComponents queue and let it "wait"
dirtyComponents.push(component);
if (component._updateBatchNumber == null) {
component._updateBatchNumber = updateBatchNumber + 1; }}Copy the code
The focus here is on one object, the batchingStrategy, whose isBatchingUpdates attribute directly determines whether the update process should go through at the moment, or whether it should wait.
When React calls batchedUpdate to perform the update action, the lock is set to “locked” (set to true), indicating that “a batch update is in progress.” When the lock is “locked”, any components that need to be updated can only be temporarily queued up in the dirtyComponents for the next batch update.
var ReactDefaultBatchingStrategy = {
// Globally unique lock identifier
isBatchingUpdates: false.// The method to initiate the update action
batchedUpdates: function(callback, a, b, c, d, e) {
// Cache lock variables
var alreadyBatchingStrategy = ReactDefaultBatchingStrategy. isBatchingUpdates
// Lock the lock.
ReactDefaultBatchingStrategy. isBatchingUpdates = true
if (alreadyBatchingStrategy) {
callback(a, b, c, d, e)
} else {
// Start the transaction and execute the callback in the transaction
transaction.perform(callback, null, a, b, c, d, e)
}
}
}
Copy the code
Here, we also need to understand Transaction mechanism in React.
Transaction, represented as a core class in the React source code, creates a black box that encapsulates any method. Therefore, methods that need to be run before and after a function run can be encapsulated by this method (these fixed methods can be run even if an exception is thrown during a function run), and only methods that need to be provided when a Transaction is instantiated.
* <pre>* wrappers (injected at creation time) * + + * | | * +-----------------|--------|--------------+ * | v | | * | +---------------+ | | * | +--| wrapper1 |---|----+ | * | | +---------------+ v | | * | | +-------------+ | | * | | +----| wrapper2 |--------+ | * | | | +-------------+ | | | * | | | | | | * | v v v v | wrapper * | +---+ +---+ +---------+ +---+ +---+ | invariants * perform(anyMethod) | | | | | | | | | | | | maintained * +----------------->|-|---|-|---|-->|anyMethod|---|---|-|---|-|--------> * | | | | | | | | | | | | * | | | | | | | | | | | | * | | | | | | | | | | | | * | +---+ +---+ +---------+ +---+ +---+ | * | initialize close | * + -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- - + *</pre>
Copy the code
Transaction is a “shell” that first wraps the target function with a Wrapper (a set of initialize and close methods is called a wrapper) and executes it using the Perform method exposed by Transaction. As the comments above show, perform executes all wrapper methods initialize before anyMethod and all wrapper close methods perform after anyMethod executes. That’s the transaction mechanism in React.
In light of the click event we just saw, the event is actually called as a callback function in the transaction. Before the call, the batch update policy transaction will set isBatchingUpdates to true, and then execute the callback method. Set isBatchingUpdates to false, then loop through all dirtyComponents and call updateComponent to update the component.
So the click event, you can actually think of it this way
add = () = > {
// Come in and lock it
isBatchingUpdates = true
console.log(Before the 'add'.this.state.num)
this.setState({
num: this.state.num + 1
});
console.log(After the 'add'.this.state.num)
// Execute the function and release it
isBatchingUpdates = false
}
Copy the code
In this case, setState is asynchronous.
Let’s take a look at setTimeout
reduce = () = > {
// Come in and lock it
isBatchingUpdates = true
setTimeout(() = > {
console.log('before the reduce'.this.state.num)
this.setState({
num: this.state.num - 1
});
console.log('after the reduce of'.this.state.num)
},0);
// Execute the function and release it
isBatchingUpdates = false
}
Copy the code
Since setTimeout is executed in a later macro task, setState, isBatchingUpdates will be set to false and will immediately perform updates, thus providing synchronization. SetState does not have the characteristics of synchronization, but in some special execution order, out of the control of asynchrony
conclusion
SetState is not purely synchronous/asynchronous and behaves differently depending on the calling scenario: it behaves asynchronously in React hook functions and synthesized events; In functions such as setTimeout and setInterval, including DOM native events, it is synchronized. This difference is essentially due to the way React transactions and batch updates work.