Is setState synchronous or asynchronous?
SetState is like a “most familiar stranger” to many React developers:
When you started React, one of the first apis you encountered was setState — data-driven views, without which you couldn’t create changes.
When the data flow of a project is in a mess, at the end of the investigation, setState is often found to be the initiator — the working mechanism is too complex and the documentation is not clear, so we can only “cross the river by feeling the stones”.
As time goes by, the working mechanism of setState gradually keeps pace with the React harmonic algorithm and becomes one of the knowledge modules with the highest differentiation in the core principles of React. Next, close to the React source code and the current highest frequency of interview questions, a fundamental understanding of the setState workflow.
1. Start with an interview question
This is a variety of interview questions, in the BAT and other first-line factories interview frequency is very high. First, we’ll give you an App component that has several different setState operations inside it, as shown in the following code:
import React from "react"; import "./styles.css"; export default class App extends React.Component{ state = { count: 0} increment = () => {console.log('increment setState before count', this.state.count) this.setState({count: this.state.count + 1 }); Console. log('increment setState count', this.state.count)} triple = () => {console.log('increment setState count', this.state.count)} triple = () => {console.log('increment setState count', this.state.count) this.state.count) this.setState({ count: this.state.count + 1 }); this.setState({ count: this.state.count + 1 }); this.setState({ count: this.state.count + 1 }); Console. log(' count after triple setState ', This.state.count)} reduce = () => {setTimeout(() => {console.log('reduce setState count', this.state.count) this.setState({ count: this.state.count - 1 }); Console. log('reduce setState count', this.state.count)},0); } render(){return <div> <button onClick={this.triple}> </button onClick={this.triple}> <button onClick={this.reduce}> </button> </div>}Copy the code
Then mount the component to the DOM:
import React from "react";
import ReactDOM from "react-dom";
import App from "./App";
const rootElement = document.getElementById("root");
ReactDOM.render(
<React.StrictMode>
<App />
</React.StrictMode>,
rootElement
);
Copy the code
The browser renders three buttons as shown below:
The question is, what would the console output look like if you clicked each button from left to right? At this point, I suggest you pause for a minute and run through the code in your head to see if it matches the actual results shown below.
If you’re a React developer, increment is not a problem — as many React 101 tutorials claim, “setState is an asynchronous method,” which means that when we’re done with setState, State itself does not change immediately. So the state output immediately after setState remains in its original state (0). State increases to 1 “just right” at some “magic moment” after the synchronized code finishes executing.
But when exactly does this “magic moment” happen, and what is the definition of “cha-cha-cha”? If you don’t understand this, the output of the triple method will be confusing to you — setState doesn’t work once, setState doesn’t work three times, and at what point does state change?
With such confusion, temporarily put aside everything to see what is the reduce method, the result is more surprising, the setState in the Reduce method is actually synchronous update! This…… Did you get the wrong basic tutorial when you first learned React, or did your computer crash?
To understand the magic of what’s happening, you have to look at how setState works.
Asynchronous motivation and rationale — the art of batch updates
The first question to recognize is: what happens after the setState call? Based on what you’ve learned so far in the column, you might be inclined to think about it in terms of the life cycle and come to a conclusion that looks like this:
As can be seen from the figure, a complete update process involves multiple steps including re-render. Re-render itself involves DOM manipulation, which incurs a significant performance overhead. If it is true that one call to setState triggers a complete update process, then every call to setState triggers a re-render, and the view is likely to get stuck before it is refreshed several times. This process is illustrated by the arrow flow diagram in the code below:
this.setState({
count: this.state.count + 1 ===> shouldComponentUpdate->componentWillUpdate->render->componentDidUpdate
});
this.setState({
count: this.state.count + 1 ===> shouldComponentUpdate->componentWillUpdate->render->componentDidUpdate
});
this.setState({
count: this.state.count + 1 ===> shouldComponentUpdate->componentWillUpdate->render->componentDidUpdate
});
Copy the code
In fact, this is an important motivation for setState asynchrony — to avoid frequent re-render.
In the actual React runtime, setState is implemented asynchronously in a manner similar to Vue’s $nextTick and event-loop in the browser: each time a setState is received, it is “saved” in a queue. When the time is ripe, the “accumulated” state results are combined, and finally only one update process is carried out for the latest state value. This process is called “batch update”, and the batch update process is shown in the arrow flow diagram in the code below:
This.setstate ({count: this.state.count +1 === => join, [count+1 task]}); This.setstate ({count: this.state.count +1 ===>, [count+1 task, count+1 task]}); This.setstate ({count: this.state.count +1 ===>, [count+1 task, count+1 task, count+1 task]}); ↓ Merge state, [count+1 task] ↓ Execute the count+1 taskCopy the code
It is worth noting that the “save up” action does not stop as long as the synchronization code is executing. (Note: Multiple +1 only works once, because multiple setState combinations in the same method do not simply add updates. For example, React only keeps the last update for the same property. So even if we wrote a setState loop like this 100 times in React:
Test = () => {console.log(' loop 100 times setState count', this.state.count) for(let I =0; i<100; I++) {this.setstate ({count: this.state.count + 1})} console.log(' count after 100 setState loops ', this.state.count)}Copy the code
It will only increase the number of state quests joining the team, not the frequency of re-render. After 100 calls, only the contents of state’s task queue have changed, and state itself does not change immediately:
3. The story behind the “synchronization phenomenon” : A source view of the setState workflow
Let’s focus on the weirdest part of the code we just wrote: setState synchronization:
Reduce = () => {setTimeout(() => {console.log('reduce setState count', this.state.count) this.setState({count: this.state.count - 1 }); Console. log('reduce setState count', this.state.count)},0); }Copy the code
From the title, it seems that setState has the “special function” of synchronization only under the “protection” of setTimeout function. If we remove setTimeout, the console behavior before and after setState will be the same as that of the increment method:
Reduce = () => {// setTimeout(() => {console.log('reduce setState count', this.state.count) this.setState({count: this.state.count - 1 }); Console. log('reduce setState count', this.state.count) //},0); }Copy the code
The output result after clicking is as follows:
Now the question becomes much clearer: why can setTimeout change the execution order of setState from asynchronous to synchronous?
Here is a conclusion: setTimeout does not change setState, but setTimeout helps setState “escape” its control by React. As long as it is setState under React control, it must be asynchronous.
The React source code provides clues to support this conclusion.
React 16 and React 17 are popular in the market, but React 15 is still the best material to learn about setState. Therefore, all of the following source code analysis will focus on React 15. How Fiber changes setState after React 16 will be explained in a later article.
1) Read the setState workflow
When we read the source code of any framework, we should read it with a question in mind and a purpose in mind. In React, functions are split in detail. The setState part involves multiple methods. To facilitate understanding, the main process is first extracted into a big picture:
Then follow the process and check the source code one by one. The first is the setState entry function:
ReactComponent.prototype.setState = function (partialState, callback) { this.updater.enqueueSetState(this, partialState); if (callback) { this.updater.enqueueCallback(this, callback, 'setState'); }};Copy the code
The entry function, in this case, acts as a dispenser, “distributing input parameters to different function functions”. Here, in the form of objects into, for example, you can see it directly invoking the enclosing updater. EnqueueSetState this method:
enqueueSetState: function (publicInstance, PartialState) {/ / according to this to get the corresponding component instance var internalInstance = getInternalInstanceReadyForUpdate (publicInstance, 'setState'); / / if this queue is a component instance var queue state array = internalInstance. _pendingStateQueue | | (internalInstance._pendingStateQueue = []); queue.push(partialState); // enqueueUpdate(internalInstance) is used to process the current component instance; }Copy the code
To summarize, enqueueSetState does two things:
-
Put the new state in the component’s state queue.
-
EnqueueUpdate is used to process the instance object to be updated.
Continue to see what enqueueUpdate does:
function enqueueUpdate(component) { ensureInjected(); IsBatchingUpdates if (! BatchingStrategy. IsBatchingUpdates) {/ / if the current did not create/update components in bulk phase, Immediately to update the component batchingStrategy. BatchedUpdates (enqueueUpdate, component); return; } // Otherwise, put the component in the dirtyComponents queue and make it "wait" dirtyComponents. Push (Component); if (component._updateBatchNumber == null) { component._updateBatchNumber = updateBatchNumber + 1; }}Copy the code
This enqueueUpdate is pretty neat, and it leads to a key object, batchingStrategy, whose isBatchingUpdates property directly determines whether the update process is going through at the moment, or whether it should wait in a queue. The batchedUpdates method can directly initiate the update process. Therefore, we can speculate that batchingStrategy may be the object specifically used to control batch updates in React. Next, we will study the batchingStrategy together.
/ * * * * * / var ReactDefaultBatchingStrategy batchingStrategy source code = {/ / globally unique logo isBatchingUpdates lock: False, // batchedUpdates: function(callback, a, b, c, d, E) {/ / cache lock variable var alreadyBatchingStrategy = ReactDefaultBatchingStrategy isBatchingUpdates / / lock "lock" ReactDefaultBatchingStrategy. isBatchingUpdates = true if (alreadyBatchingStrategy) { callback(a, b, c, d, Perform (callback, null, a, B, c, d, e)}}Copy the code
The batchingStrategy object is not complex and can be thought of as a “lock manager.”
The initial value of isBatchingUpdates is false, meaning that “no batch updates are currently being performed.” 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 placed in the dirtyComponents queue for the next batch update, rather than “jumping the queue” at will. The idea of “task locking” here is the cornerstone of React’s ability to implement orderly batch processing in the face of a large number of states.
Now that you understand the overall management mechanism for batch updates, note that in batchedUpdates, there is a notable call:
transaction.perform(callback, null, a, b, c, d, e)
Copy the code
This line of code leads to a more hardcore concept — Transaction in React.
2) Understand Transaction in React
Transaction is widely distributed in the React source code. If you find initialize, Perform, CLOSE, closeAll, or notifyAll in the function call stack during the Debug React project, you are probably in a Trasaction.
Transaction is described as a core class in the React source code. Transaction 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.
React: Transaction: Transaction: Transaction: Transaction: Transaction: Transaction: Transaction
* <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
In plain English, Transaction is a “shell” that first wraps the target function with a Wrapper (a set of initialize and close methods is called a wrapper), You also need to perform it using the Perform method exposed by the Transaction class. 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.
3) The nature of the “synchronization phenomenon”
The following understanding of transaction mechanism, continue to run in ReactDefaultBatchingStrategy this object. ReactDefaultBatchingStrategy transaction is actually a batch update strategy, its wrapper, there are two: FLUSH_BATCHED_UPDATES and RESET_BATCHED_UPDATES.
var RESET_BATCHED_UPDATES = { initialize: emptyFunction, close: function () { ReactDefaultBatchingStrategy.isBatchingUpdates = false; }}; var FLUSH_BATCHED_UPDATES = { initialize: emptyFunction, close: ReactUpdates.flushBatchedUpdates.bind(ReactUpdates) }; var TRANSACTION_WRAPPERS = [FLUSH_BATCHED_UPDATES, RESET_BATCHED_UPDATES];Copy the code
By wrapping these two wrappers into the Transaction execution mechanism, it is not difficult to produce a flow that looks like this:
- “After callback, set isBatchingUpdates to false, FLUSH_BATCHED_UPDATES to flushBatchedUpdates, Then it loops through all the dirtyComponents, Call updateComponent to perform all of the life cycle method (componentWillReceiveProps ShouldComponentUpdate ->componentWillUpdate-> Render ->componentDidUpdate)
At this point, the batch update mechanism under isBatchingUpdates control is well understood. But the question of why setState should behave synchronously does not seem to be fundamentally answered by the source code currently presented. This is because the batchingUpdates method, it’s not just called after setState. If we search for batchingUpdates globally in the React source code, we’ll find that there are many places to call it, but these are the only two places that are relevant to the update flow:
// ReactMount.js _renderNewRootComponent: function( nextElement, container, shouldReuseMarkup, The context) {/ / instantiate the component var componentInstance = instantiateReactComponent (nextElement); / / the initial rendering direct call for synchronous rendering ReactUpdates batchedUpdates. BatchedUpdates (batchedMountComponentIntoNode componentInstance, container, shouldReuseMarkup, context ); . }Copy the code
This code is a method that is executed when the component is first rendered, and we see an internal call to batchedUpdates because the lifecycle functions are called sequentially during the component’s rendering. The developer will most likely call setState in a declared periodic function. Therefore, we need to enable Batch to ensure that all updates go into the dirtyComponents to ensure that all setStates in the initial rendering process are in effect.
The following code is part of the React event system. When we bind events to the component, setState may also be triggered in the event. To ensure that setState is valid every time, React will also manually enable batch updates here.
// ReactEventListener.js dispatchEvent: function (topLevelType, nativeEvent) { ... Try {/ / handle events ReactUpdates. BatchedUpdates (handleTopLevelImpl, bookKeeping); } finally { TopLevelCallbackBookKeeping.release(bookKeeping); }}Copy the code
At this point, it becomes clear that the isBatchingUpdates variable has been quietly changed to true by React before the React lifecycle function and compositing events are executed, so the setState operation we do will not take effect immediately. When the function completes, the transaction’s close method changes isBatchingUpdates to false.
Taking the increment method in the first example, the process looks like this:
Increment = true console.log('increment setState ', increment = true console.log('increment setState ', increment = true console.log('increment setState ', increment = true console.log)) this.state.count) this.setState({ count: this.state.count + 1 }); Console. log('increment setState count', this.state.count)Copy the code
Obviously, under the isBatchingUpdates constraint, setState can only be asynchronous. When setTimeout gets in the way, things change a little bit:
Reduce = () => {isBatchingUpdates = true setTimeout(() => {console.log('reduce setState count', this.state.count) this.setState({ count: this.state.count - 1 }); Console. log('reduce setState count', this.state.count)},0); IsBatchingUpdates = false}Copy the code
You can see that the isBatchingUpdates that are locked at the beginning have no binding at all on the execution logic inside setTimeout. Because isBatchingUpdates are changed in synchronized code, the setTimeout logic is executed asynchronously. By the time the this.setState call actually occurs, isBatchingUpdates will have already been reset to false, making the setState in the current scenario capable of launching synchronous updates immediately. So it’s true — setState doesn’t have synchronization, it just “escapes” from React’s asynchronous control in certain situations.
4, summarize
The principle is simple, but the principle is complicated. Finally, answer the question posed by the title face to face again to summarize the entire setState workflow.
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 determined by the way React transactions and bulk updates work.
At this point, you have an intimate understanding of setState. The entire discussion in this article builds on React 15. Since React 16, the whole React core algorithm has been rewritten, and setState has inevitably been “fiberized”. Then what is “Fiber” exactly and how it changes the core technology modules of React including setState, are the key issues discussed in the following articles.
Learning the source (the article reprinted from) : kaiwu.lagou.com/course/cour…