This is the seventh day of my participation in the First Challenge 2022. For details: First Challenge 2022.

1. Batch update

Let’s look at batch updates first.

Let’s start with a code example:

import React from "react";

export default class App extends React.Component {
  state = {
    count: 0}; firstIncrement =() = > {
    console.log("Increment by 1".this.state.count);
    this.setState({
      count: this.state.count + 1});console.log(When I increase by 1.this.state.count);
  };

  secondIncrement = () = > {
    console.log("Increment by 2".this.state.count);
    this.setState({
      count: this.state.count + 1});this.setState({
      count: this.state.count + 1});console.log("When I increase by 2".this.state.count);
  };

  reduce = () = > {
    setTimeout(() = > {
      console.log("Minus 1 before".this.state.count);
      this.setState({
        count: this.state.count - 1});console.log("After I decrease by 1".this.state.count);
    }, 0);
  };

  render() {
    return (
      <div>
        <button onClick={this.firstIncrement}>Button 1- Increases by 1</button>
        <button onClick={this.secondIncrement}>Button 2- Add 2</button>
        <button onClick={this.reduce}>Reduced by 1</button>
      </div>); }}Copy the code

Click the operation result successively:

As can be seen from the above result, when we directly operate setState to print the result, it is asynchronous and the latest value cannot be obtained. If you do setState multiple times the result will be merged and executed only once. Put it in setTime and it becomes synchronous and you get the value immediately.

Look at setState updates from a lifecycle perspective:

As can be seen from the figure, a complete update process involves several steps including re-render.

Re-render itself involves DOM manipulation, which incurs a significant performance overhead.

If a single setState triggers a complete update process, then every call to setState triggers a re-render, and our view is likely to get stuck without refreshing more than a few times.

this.setState({
  count: this.state.count + 1    ===>    shouldComponentUpdate->render->componentDidUpdate
});
this.setState({
  count: this.state.count + 1    ===>    shouldComponentUpdate->render->componentDidUpdate
});
this.setState({
  count: this.state.count + 1    ===>    shouldComponentUpdate->render->componentDidUpdate
});
Copy the code

In summary, React is an important motivation for asynchrony in designing setState to avoid frequent re-render.

Therefore, React implements setState asynchronously using batch update operations, using a queue to store it. Each time a setState enters the queue, the queue operation is performed. When the time is ripe, the state values are combined, and the update process is performed only once for the latest state value.

this.setState({
  count: this.state.count + 1===> Join the queue, [count+1[});this.setState({
  count: this.state.count + 1===> Join the queue, [count+1Task, count+1[});this.setState({
  count: this.state.count + 1===> Join the queue, [count+1Task, count+1Task, count+1[}); ↓ Combine state, [count+1] ↓ Run count+1The task ofCopy the code

Note: As long as our synchronization code is executing, the enqueue operation will not stop, so finally only one +1 will take effect.

test = () = > {
  console.log('Loop count before setState 100 times'.this.state.count) / / 0
  for(let i = 0; i < 100 ; i++) {
    this.setState({
      count: this.state.count + 1})}console.log('Count after loop setState 100 times'.this.state.count) / / 0
}

Copy the code

2. Nature of synchronous phenomenon

Synchronization of setState:

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

Synchronization happens when we add the setTimeout function. It’s not that setTimeout changes setState, it’s that setState in setTimeout leaves the React event system, so it becomes synchronized. Why does this happen?

Let’s start with the concept of locks:

A batchingStrategy is an object inside React that manages batch updates. You can think of it as a lock manager.

The lock refers to the React globally unique isBatchingUpdates variable. 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 component that needs to be updated can only temporarily queue up for the next batch update, rather than “jump the queue” at will.

The isBatchingUpdates variable is changed to true by React before the React lifecycle function and compositing events are executed, so the setState operation won’t take effect immediately. When the function completes, the transaction’s close method changes isBatchingUpdates to false.

With isBatchingUpdates constraints, setState can only be asynchronous. Sample code:

increment = () = > {
  // Come in and lock it
  isBatchingUpdates = true
  console.log('increment setState count'.this.state.count)
  this.setState({
    count: this.state.count + 1
  });
  console.log(Increment setState count.this.state.count)
  // Execute the function and release it
  isBatchingUpdates = false
}
Copy the code

After adding setTimeout, things change a little bit: isBatchingUpdates, which have no binding at all on the execution logic inside setTimeout.

reduce = () = > {
  // Come in and lock it
  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);
  // Execute the function and release it
  isBatchingUpdates = false
}
Copy the code

IsBatchingUpdates are changed in synchronized code, whereas 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. SetState does not have synchronization, but it can “escape” from the React asynchronous control in certain situations.

Before the React event is executed, isBatchingEventUpdates=true is used to enable the event batch update. When the event ends, isBatchingEventUpdates= false is used to enable the event batch update. Turn off the switch. So the batch update rule is broken.

3. Manually enable batch update

So, how to continue to enable batch update mode in the above asynchronous environment? The react-DOM provides a batch update method called unstable_batchedUpdates, which can be manually batch updated. You can modify setTimeout as follows:

import ReactDOM from 'react-dom'
const { unstable_batchedUpdates } = ReactDOM

setTimeout(() = >{
    unstable_batchedUpdates(() = >{
        this.setState({ number:this.state.count + 1 })
        console.log(this.state.count)
        this.setState({ number:this.state.count + 1})
        console.log(this.state.count)
        this.setState({ number:this.state.numbercount+ 1 })
        console.log(this.state.count) 
    })
})
Copy the code

4, summarize

  • 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.
  • SetState is not really asynchronous, it just looks asynchronous. In the source code, use isBatchingUpdates to determine whether setState is stored in the state queue first or updated directly. If the value is true, it is performed asynchronously, and if it is false, it is updated directly. So in what case will isBatchingUpdates be true? True where React can be controlled, such as in React lifecycle events and composited events, both merge and delay updates. However, when React is out of control, such as native events, such as addEventListener, setTimeout, setInterval, etc., updates can only be synchronized. The React team added two additional points to the conventional wisdom that asynchronous design is for performance optimization and reduced rendering times. Maintain internal consistency. If you change state to synchronous update, though the update to state is synchronous, the props is not. Enable concurrent updates to complete asynchronous rendering.