Let's first think about an old-fashioned question, is setState synchronous or asynchronous?
Think again, is useState synchronous or asynchronous?
Let's write a few demo s to test it out.
First look at useState
Execute two useState examples in succession, both synchronously and asynchronously
function Component() { const [a, setA] = useState(1) const [b, setB] = useState('b') console.log('render') function handleClickWithPromise() { Promise.resolve().then(() => { setA((a) => a + 1) setB('bb') }) } function handleClickWithoutPromise() { setA((a) => a + 1) setB('bb') } return ( <Fragment> <button onClick={handleClickWithPromise}> {a}-{b} Asynchronous execution </button> <button onClick={handleClickWithoutPromise}> {a}-{b} Synchronous execution </button> </Fragment> ) }
in conclusion:
- When the synchronous execution button is clicked, it is only re-render ed once
- When the async execution button is clicked, it is render ed twice
Execute the same useState example twice in a row, both synchronously and asynchronously
function Component() { const [a, setA] = useState(1) console.log('a', a) function handleClickWithPromise() { Promise.resolve().then(() => { setA((a) => a + 1) setA((a) => a + 1) }) } function handleClickWithoutPromise() { setA((a) => a + 1) setA((a) => a + 1) } return ( <Fragment> <button onClick={handleClickWithPromise}>{a} Asynchronous execution</button> <button onClick={handleClickWithoutPromise}>{a} Synchronous execution</button> </Fragment> ) }
- When the synchronous execution button is clicked, both setA are executed, but the render is merged once, printing 3
- When the asynchronous execution button is clicked, the two setA will each render once and print 2 and 3 respectively.
refer to Front-end react interview questions answered in detail
Look at setState again
Execute two setState examples in succession, both synchronously and asynchronously
class Component extends React.Component { constructor(props) { super(props) this.state = { a: 1, b: 'b', } } handleClickWithPromise = () => { Promise.resolve().then(() => { this.setState({...this.state, a: 'aa'}) this.setState({...this.state, b: 'bb'}) }) } handleClickWithoutPromise = () => { this.setState({...this.state, a: 'aa'}) this.setState({...this.state, b: 'bb'}) } render() { console.log('render') return ( <Fragment> <button onClick={this.handleClickWithPromise}>Asynchronous execution</button> <button onClick={this.handleClickWithoutPromise}>Synchronous execution</button> </Fragment> ) } }
- When the synchronous execution button is clicked, it is only re-render ed once
- When the async execution button is clicked, it is render ed twice
Same as the result of useState
Execute the same setState twice in a row, both synchronously and asynchronously
class Component extends React.Component { constructor(props) { super(props) this.state = { a: 1, } } handleClickWithPromise = () => { Promise.resolve().then(() => { this.setState({a: this.state.a + 1}) this.setState({a: this.state.a + 1}) }) } handleClickWithoutPromise = () => { this.setState({a: this.state.a + 1}) this.setState({a: this.state.a + 1}) } render() { console.log('a', this.state.a) return ( <Fragment> <button onClick={this.handleClickWithPromise}>Asynchronous execution</button> <button onClick={this.handleClickWithoutPromise}>Synchronous execution</button> </Fragment> ) } }
- When the synchronous execution button is clicked, the two setState s are merged, only the last one is executed, and 2 is printed
- When the asynchronous execution button is clicked, the two setState will each render once, and print 2 and 3 respectively.
Unlike useState, useState will also process state s one by one during synchronous execution, while setState will only process the last time
Why are there different results between synchronous execution and asynchronous execution?
This involves react's batchUpdate mechanism, which merges updates.
- First, why do you need merge updates?
If the update is not merged, the component will have to re-render every time useState is executed, which will cause invalid rendering and waste time (because the last rendering will overwrite all previous rendering effects).
So react will put together some useState/setState that can be updated together, and merge and update.
- how to merge update
Here react uses the transaction mechanism.
Batch Update in React is implemented through "Transaction". In the part of the React source code about Transaction, the role of Transaction is explained with a large paragraph of text and a character painting:
* wrappers (injected at creation time) * + + * | | * +-----------------|--------|--------------+ * | v | | * | +---------------+ | | * | +--| wrapper1 |---|----+ | * | | +---------------+ v | | * | | +-------------+ | | * | | +----| wrapper2 |--------+ | * | | | +-------------+ | | | * | | | | | | * | v v v v | wrapper * | +---+ +---+ +---------+ +---+ +---+ | invariants * perform(anyMethod) | | | | | | | | | | | | maintained * +----------------->|-|---|-|---|-->|anyMethod|---|---|-|---|-|--------> * | | | | | | | | | | | | * | | | | | | | | | | | | * | | | | | | | | | | | | * | +---+ +---+ +---------+ +---+ +---+ | * | initialize close | * +-----------------------------------------+
In the vernacular, a segment of logic is added before and after the actual useState/setState to wrap it up. As long as setState in the same transaction will be merged (note that useState will not merge state) processing.
- Why setTimeout cannot perform transactional operations
Due to the event delegation mechanism of react, the event executed by calling onClick is under the control of react.
And setTimeout is beyond the control of react, react cannot add transaction logic before and after the code of setTimeout (unless react rewrites setTimeout).
So when encountering setTimeout/setInterval/Promise.then(fn)/fetch callback/xhr network callback, react can't control it.
The relevant react source code is as follows:
if (executionContext === NoContext) { // Flush the synchronous work now, unless we're already working or inside // a batch. This is intentionally inside scheduleUpdateOnFiber instead of // scheduleCallbackForFiber to preserve the ability to schedule a callback // without immediately flushing it. We only do this for user-initiated // updates, to preserve historical behavior of legacy mode. flushSyncCallbackQueue() }
executionContext represents the current stage of react, and NoContext can be understood as the state where react has no work. And flushSyncCallbackQueue will call our this.setState synchronously, which means that our state will be updated synchronously. So, we know that when executionContext is NoContext, our setState is synchronous
Summarize
Let's summarize the results of the above experiments:
- In the normal react event flow (such as onClick, etc.)
- setState and useState are executed asynchronously (the result of state is not updated immediately)
- Execute setState and useState multiple times, only call the re-render once
- The difference is that setState will merge state, while useState will not
- in async events like setTimeout, Promise.then etc.
- setState and useState are executed synchronously (result of updating state immediately)
- Execute setState and useState multiple times, and each time setState and useState is executed, render will be called once
Does it feel a bit confusing, just write the code and experience it yourself~