Use React setState like this to develop a direct call line!

(Long time no see, title skin, content or conscience)
As we all know, React realizes the management of components by managing state, and setState is the most basic method for changing state. Although it is basic, it is not easy to master. This paper will make a relatively in-depth analysis of this method in combination with some source code.

Basic Usage

  1. First of all, its basic API is given in the official document:
// Accept 2 parameters, updater and callback
setState(updater[, callback])

// First: updater can be an object
setState({
    key: val
}, newState=>{
    // You can get the updated newState in the callback
})

// Second, updater can be a function that returns an object value
setState((state, props)=>{
    return {
        key: val
    }
}), newState=>{
})

Where updater means that the new state value can be a function that returns an object, or it can be an object directly. This part of the content will be merged into state through shallow comparison.
The official documents clearly tell us:

setState queues changes to the component state and tells react that it needs to re render the component and its subcomponents using the updated state. Treat setState() as a request rather than a command to update the component immediately. For better perceptual performance, react will delay calling it and then update multiple components through one pass. React does not guarantee that changes to state will take effect immediately.

So the second parameter of the api, callback, allows us to do some update operations after setState is completed.

After a little review of the basic knowledge above, we will start to discuss it in detail.

On the first function parameter

In order to avoid boredom, we continue to study with questions:

  • Question 1: what's the difference between using function parameters and object parameters in setState?
    Before answering this question, please take a look at the example of a very common timer:
class Demo extends Component {
    constructor(props) {
        super(props);
        this.state = {
            count: 0 //Initial value
        };
    }

    increaseCount = () => {
        this.setState({count: this.state.count + 1});
    }

    handleClick = ()=>{
        this.increaseCount();
    }

    render() {
        return (
            <div className="App-header">
                <button onClick={this.handleClick}>Click to increase</button>      
                count:{this.state.count}
            </div>
        );
    }
}

This code does not seem to have any problems. It can also increase by 1 every time you click, which is fully in line with the expected effect. But next! We hope that by changing handleClick, each time we click, count will increase by 2 times, that is:

  handleClick = ()=>{
        this.increaseCount()
        this.increaseCount()
    }

At this time, you will be surprised to find that after each click, the count still increases by 1! What's the problem?

In fact, this is this.setState({count: this.state.count + 1}). Because setState is not a synchronization method mentioned earlier, this.state.count here does not guarantee the latest value. At this time, we can use the second method:

   // Here we use the function parameter method
   increaseCount = () => {
        this.setState((state)=>{return{count: state.count + 1}});
    }

Try again at this time, and find that the timer can execute as expected. Now you can answer question 1: if you need to update the new state according to the existing state during setState, you should use the function parameter to ensure that you get the latest state value.

Answer 1: if you need to rely on the value of the current state to update the next value, you need to use the function as a parameter, because the function can guarantee to get the latest state

About bulk updates

The next thing to be studied is the play -- setState update process. Maybe all the documents you read tell you that setState will not guarantee immediate execution, but will update all component s in batches at a certain time.

So the question is: why set the batch mechanism and how does the batch update process work?

Question 2: why should setstates set the batch update mechanism?

This is actually in the performance consideration of large-scale applications. First of all, we all know that component render is very time-consuming. Imagine this scenario:

If a composite component is composed of a Parent and a Child, both the Parent component and the Child component need to execute setState in a click event. If there is no mechanism for batch update, the setState of the Parent component will trigger the re render of the Parent component and also the render of the Child component, and the setState of the Child component itself will trigger its own re-render. This will lead to Child re render twice. The batch update mechanism is to deal with this situation.

So then came the question:

Question 3: how to perform the batch update process

In order to answer this question, I sorted out the source code related to setState in react (the steps of source code learning are put at the end, and interested partners can read it, and you can skip the conclusion if you want to see it directly), put aside some details that have little impact on the mainstream program (remove some code such as error throwing to improve reading efficiency), and sort out such a general process:( If the image is compressed, please click https://www.processon.com/view/link/5de4a992e4b0df12b4afbc2a)

It can be roughly divided into the following stages:

  1. First of all, determine whether the context environment in which setState is executed is in the event system or Mount cycle (this is very important)!!! , detailed later)
  2. A component executes setState
  3. Put the new state value partialState into the instance variable corresponding to the component. Here's a brief introduction. react creates a corresponding instance object for each component in memory, which is used to save some corresponding attributes, so as to use the attributes corresponding to the component clearly when updating and other processes.
  4. Put the cached variable's component into the global variable dirtyComponents array, and judge whether to immediately batch update according to the judgment of the first step (if it is, directly update it; if it is in the handle event or mount stage, wait until the end of the stage to perform the update)

PS: the key step in this process is that the source code of react 15.6 is implemented by using the writing method of transaction transition, but I don't think it's necessary to explain the content of setState, so I won't explain it in depth in this article, but I'll separate it out to make it easier for readers to understand. If necessary, write another article to explain the transition

As can be seen from the above description, I put the context environment for judging the execution of setState at the beginning. Why do I do this? Next, let's look at another interesting example. Change the first timer example slightly:

class Main extends Component {
    constructor(props) {
        super(props);
        this.state = {
            count: 0 //Initial value
        };
    }

    // Note the changes to this function
    increaseCount = () => {
        this.setState({count: this.state.count + 1});
        console.log("1st output", this.state.count) 
        setTimeout(()=>{
            this.setState({count: this.state.count + 1});
            console.log("2nd output", this.state.count)
            this.setState({count: this.state.count + 1});
            console.log("3rd output", this.state.count)
        },0)
    }

    handleClick = ()=>{
        this.increaseCount();
    }

    render() {
        return (
            <div className="App-header">
                <button onClick={this.handleClick}>Click to increase</button>      
                count:{this.state.count}
            </div>
        );
    }
}

After clicking the button, can you directly answer the above three times of output respectively?

Answer: the three outputs are 0, 2, 3

  • First of all, for the first console, it can be seen from the above process that setState is in the click event handle phase when it is executed, so this update will be put into the update queue and the update will be delayed, so console can't get the latest result immediately (similarly, if we perform setState operation in the life cycle phase such as component will mount and console can't get the latest value immediately, The first output is 0, because batch update is also required
  • Secondly, the second and third setState are put in setTimeout. As I said before when I wrote asynchronous event queue, because js's single thread, all asynchronous operations will be put in asynchronous queue. Therefore, when I call setState these two times, function call stack is different from the first time. They are not in event handle or component Mount stage, so I call set After state, the batch update will be performed immediately (in fact, the current component will also be put into dirtyComponents at this time, but there is only one dirtyComponent at this time, and the batch update will be performed). Therefore, the value of the change can be obtained immediately for the next two updates, so the output of 2 and 3 respectively.

As a matter of fact, we should think about one question:
Question 4: why does React arrange batch update mechanism in Mount process and event processing system?
Answer 4: recall that the original intention of setting up batch update is to reduce unnecessary renderers in the whole application and improve performance. The most likely time to need renderers is actually:

  1. Mount is a component stage, and there is a render process;
  2. Within the event handling function, there are often multiple components, and the setState operation may be performed for an event;

To sum up, it's not hard to guess, in fact, React should want to force setState to be controlled asynchronously in all places. In terms of the current version (the source code used in this article is 15.6), only manual setTimeout or promise.then (usually used to update a state after requesting data) can override this control.

Appendix related source reading order

This part is my personal reading order of setState in react source code. It's only for reference. I hope it can help the small partners who study source code:

// 1.  react-15.6.0/src/isomorphic/modern/class/ReactBaseClasses.js
ReactComponent.prototype.setState = function(partialState, callback) {
  // Omit error capture and exception handling
  // updater is actually injected. It can find enqueueSetState method directly and globally
  this.updater.enqueueSetState(this, partialState);
  if (callback) {
    this.updater.enqueueCallback(this, callback, 'setState');
  }
};

// Find the following by finding the enqueueSetState method globally
// 2.react-15.6.0/src/renderers/shared/stack/reconciler/ReactUpdateQueue.js
enqueueSetState:function(publicInstance, partialState){
// It can be simply understood as follows: an array of ﹣ pendingStateQueue ﹣ is saved on the current component, with the value of partialState. In fact, internalInstance is a variable corresponding to the current component in memory, which is specially used to store the properties corresponding to the current component
    var internalInstance = getInternalInstanceReadyForUpdate(
      publicInstance,
      'setState',
    );

    if (!internalInstance) {
      return;
    }

    
    var queue =
      internalInstance._pendingStateQueue ||
      (internalInstance._pendingStateQueue = []);
    queue.push(partialState);

    enqueueUpdate(internalInstance);
}

// Find the enqueueUpdate method
// 3. react-15.6.0/src/renderers/shared/stack/reconciler/ReactUpdateQueue.js
enqueueUpdate: function enqueueUpdate(internalInstance) {
  ReactUpdates.enqueueUpdate(internalInstance);
}

// Find the enqueueUpdate for react updates
// 4.react-15.6.0/src/renderers/shared/stack/reconciler/ReactUpdates.js
// Here, if (!batchingStrategy.isBatchingUpdates) is the key branch statement for batch update execution
ReactUpdates.enqueueUpdate = function (component){
  // If it is not in batch update, execute it directly
  if (!batchingStrategy.isBatchingUpdates) {
    batchingStrategy.batchedUpdates(enqueueUpdate, component);
    return;
  }

  // If it's saved there, it's postponed
  dirtyComponents.push(component);
  if (component._updateBatchNumber == null) {
    component._updateBatchNumber = updateBatchNumber + 1;
  }
}

// 5. First look! batchingStrategy.isBatchingUpdates route search
// react-15.6.0/src/renderers/shared/stack/reconciler/ReactDefaultBatchingStrategy.js
// It should be noted here that every time you call batchedUpdates, the ReactDefaultBatchingStrategy.isBatchingUpdates will become true; therefore, you must globally find the place where you call batchingStrategy.batchedUpdates(). After searching, you will find that it is in the process of Mount and eventHandle, which is the key point of the first step in the previous flowchart

var ReactDefaultBatchingStrategy = {
  isBatchingUpdates: false,

  /**
   * Call the provided function in a context within which calls to `setState`
   * and friends are batched such that components aren't updated unnecessarily.
   */
  batchedUpdates: function(callback, a, b, c, d, e) {
    var alreadyBatchingUpdates = ReactDefaultBatchingStrategy.isBatchingUpdates;

    ReactDefaultBatchingStrategy.isBatchingUpdates = true;

    // The code is written this way to avoid extra allocations
    if (alreadyBatchingUpdates) {
      return callback(a, b, c, d, e);
    } else {
      // This place is written in transaction, which seems a little bit convoluted. It is suggested to understand the concept of transaction a little
      return transaction.perform(callback, null, a, b, c, d, e);
    }
  },
};
    
// 6.flushBatchedUpdates traverses the dirtyComponents loop and executes runBatchedUpdates
ar flushBatchedUpdates = function() {
    while (dirtyComponents.length || asapEnqueued) {
    if (dirtyComponents.length) {
      var transaction = ReactUpdatesFlushTransaction.getPooled();
      transaction.perform(runBatchedUpdates, null, transaction);
      ReactUpdatesFlushTransaction.release(transaction);
    }

    if (asapEnqueued) {
      asapEnqueued = false;
      var queue = asapCallbackQueue;
      asapCallbackQueue = CallbackQueue.getPooled();
      queue.notifyAll();
      CallbackQueue.release(queue);
    }
  }
}


// This is the last place to traverse the update, but it's not very interesting
function runBatchedUpdates(){
    // Traverse all dirtyComponents, update components in turn, and cache callbacks
}

Summary

(nearly 3 months have passed since I read the previous article) this paper mainly analyzes the batch update process and common problems of setState, hoping to help you. It is also helpful to find opportunities for follow-up of transaction and specific update and comparison process. If readers are interested in other topics, please come up with discussion.

Practice: if the content is wrong, please note (if you do not understand, feel uncomfortable or want to Tucao), if you are helpful, please welcome the praise and collection. Please ask for permission if you have the agreement. If you have any questions, please welcome the private letter exchange. The home page has the address of the mailbox.

Tags: Javascript React

Posted on Tue, 03 Dec 2019 01:42:04 -0500 by Louis11