The reAT source code studied in this article is v0.8.0. Since this release, we can see the evolution of the setState mechanism.

Introduction to setState evolution

There are two terms I believe everyone knows about react’s setState mechanism: “batch update” and “asynchronous execution.” Both terms actually describe the same thing. Why do you say that? As anyone who has delved into the source code knows, “batch update” is the cause and “asynchronous execution” is the effect. The world likes to see appearances and results, so I usually use the words “asynchronous execution” or “synchronous execution” to describe it.

React development has been around for a while, and I vaguely remember early versions where setState was executed synchronously, then asynchronously in some scenarios and synchronously in others. Finally, now, it’s all done asynchronously. In v0.13.0, react changed all setState to asynchronous. As evidenced by the text:

Calls to setState in life-cycle methods are now always batched and therefore asynchronous. Previously the first call on the first mount was synchronous.

In v0.8.0, setState is executed asynchronously in the Event Listener, and synchronously in First Call on the First Mount, the componentDidMount lifecycle function. Don’t believe it? Let’s use ActV0.8.0 to verify the following:

import React from 'react';

const Count  = React.createClass({
  getInitialState() {
        return {
            count: 0
        }
   },

  render() {
    return <button onClick={()=> {
      this.setState({count: this.state.count + 1});
      console.log(this.state.count);
      this.setState({count: this.state.count + 1});
      console.log(this.state.count);
      this.setState({count: this.state.count + 1});
      console.log(this.state.count);
      }}>{this.state.count}</button>
  }

  componentDidMount() { this.setState({count: this.state.count + 1}); console.log(this.state.count); this.setState({count: this.state.count + 1}); console.log(this.state.count); this.setState({count: this.state.count + 1}); console.log(this.state.count); }})export default Count; 
Copy the code

After the initial mount, the result for the log inside componentDidMount looks like this:

1
2
3
Copy the code

At this point, the number on the button is 3. This proves that setState is executed synchronously in the life cycle function componentDidMount.

If we then click on the button, the number of the button simply increases by 1 (not by 3) to 4. The result of the log is:

3
3
3
Copy the code

As you can imagine, setState is called asynchronously in the Click event Listener, so we cannot retrieve the latest state value immediately after. If we want to get the latest state value:

  • Either in the setState method callback;
  • Either get it in the componentDidUpdate lifecycle function;
  • Or fetch it at the end of the Event loop. For example, in event Listener, use setTimeout to retrieve:
setTimeout(() => {
    console.log('into setTimeout',this.state.count);
}, 0);
Copy the code

What would happen if we ran the same sample code with ActV16.8.6? :

import React from 'react';

class Count extends React.Component {
  constructor(props) {
    super(props);
    this.state = {
      count: 0
    }
  }

  render() {
    return <button onClick={()=> {
      this.setState({count: this.state.count + 1});
      console.log(this.state.count);
      this.setState({count: this.state.count + 1});
      console.log(this.state.count);
      this.setState({count: this.state.count + 1});
      console.log(this.state.count);
      }}>{this.state.count}</button>
  }

  componentDidMount() { this.setState({count: this.state.count + 1}); console.log(this.state.count); this.setState({count: this.state.count + 1}); console.log(this.state.count); this.setState({count: this.state.count + 1}); console.log(this.state.count); }}export default Count; 
Copy the code

The result is that after componentDidMount executes, the number on the button is 1, and the console prints:

0
0
0
Copy the code

Then click button, the button number increases by 1 on the original basis, becomes 2, the console prints:

1
1
1
Copy the code

That is, setState is executed asynchronously in update v16.8.6. In fact, this statement is not very rigorous. Because, as we said above, asynchronous execution is only a consequence of batch updates. In React, in order for setState to be executed asynchronously, it must be currently in a “batch update” transaction, Writing setState calls in asynchronous javascript code (setTimeout,promise, etc.) can escape the react “batch update” transaction, where setState is executed synchronously. In the event Listener of the previous version of reactV16.8.6, we tried wrapping multiple setState calls with setTimeout.

import React from 'react';

class Count extends React.Component {
  constructor(props) {
    super(props);
    this.state = {
      count: 0
    }
  }

  render() {
    return <button onClick={()=> {
      setTimeout(()=> {
        this.setState({count: this.state.count + 1});
        console.log(this.state.count);
        this.setState({count: this.state.count + 1});
        console.log(this.state.count);
        this.setState({count: this.state.count + 1});
        console.log(this.state.count);
    }, 0);
      }}>{this.state.count}</button>
  }

  componentDidMount() {
    setTimeout(()=> { this.setState({count: this.state.count + 1}); console.log(this.state.count); this.setState({count: this.state.count + 1}); console.log(this.state.count); this.setState({count: this.state.count + 1}); console.log(this.state.count); }, 0); }}export default Count; 
Copy the code

What is the result of the same operation? The result is that the number of the button is finally displayed as 6, which is printed as follows:

One, two, three, four, five, sixCopy the code

That is, setState wrapped by setTimeout is executed synchronously. Therefore, it is not exact to say that setState is executed asynchronously after version 0.13.0 of ACTV. we can only say that it is executed asynchronously under normal circumstances.

The principle of batch update

Okay, back to the point. After a brief introduction to the change history of setState mechanism, let’s formally delve into the implementation of setState mechanism in ActV0.8.0.

First of all, let’s take a look at the implementation of the setState method is how to (in ReactCompositeComponent. Js) :

/**
   * Sets a subset of the state. Always use this or `replaceState` to mutate
   * state. You should treat `this.state` as immutable.
   *
   * There is no guarantee that `this.state` will be immediately updated, so
   * accessing `this.state` after calling this method may return the old value.
   *
   * There is no guarantee that calls to `setState` will run synchronously, * as they may eventually be batched together. You can provide an optional * callback that will be executed when the call  tosetState is actually
   * completed.
   *
   * @param {object} partialState Next partial state to be merged with state.
   * @param {?function} callback Called after state is updated.
   * @final
   * @protected
   */
  setState: function(partialState, callback) {
    // Merge with `_pendingState` if it exists, otherwise with existing state.
    this.replaceState(
      merge(this._pendingState || this.state, partialState),
      callback
    );
  }
Copy the code

The setState method is very simple to implement, with no fancy high bar code. It first performs a shallow merges (overwriting the outermost key of the object) between the passed partialState and the state being processed (_pendingState) or the current state. It is then passed to the replaceState method. The replaceState method is implemented like this:

 /**
   * Replaces all of the state. Always use this or `setState` to mutate state.
   * You should treat `this.state` as immutable.
   *
   * There is no guarantee that `this.state` will be immediately updated, so
   * accessing `this.state` after calling this method may return the old value.
   *
   * @param {object} completeState Next state.
   * @param {?function} callback Called after state is updated.
   * @final
   * @protected
   */
  replaceState: function(completeState, callback) {
    validateLifeCycleOnReplaceState(this);
    this._pendingState = completeState;
    ReactUpdates.enqueueUpdate(this, callback);
  },
Copy the code

In the replaceState method, the first step is to verify LifeCycleState and give some warning. This function doesn’t matter, so skip the list. The second step is to change the object obtained by the shallow merge of setState method into the state (_pendingState) to be processed. The third step is the key to the setState mechanism, which is this code:

ReactUpdates.enqueueUpdate(this, callback);
Copy the code

“EnqueueUpdate” can be translated as “push into queue, wait for updates”. This queue is actually dirtyComponents, which we’ll see in the enqueueUpdate implementation code in a moment. The enqueueUpdate method is implemented as follows (in reactupdates.js) :

/**
 * Mark a component as needing a rerender, adding an optional callback to a
 * list of functions which will be executed once the rerender occurs.
 */
function enqueueUpdate(component, callback) {
  ("production"! == process.env.NODE_ENV ? invariant( ! callback || typeof callback ==="function".'enqueueUpdate(...) : You called `setProps`, `replaceProps`, ' +
    '`setState`, `replaceState`, or `forceUpdate` with a callback that ' +
    'is not callable.') : invariant(! callback || typeof callback ==="function"));

  ensureBatchingStrategy(); 

  if(! batchingStrategy.isBatchingUpdates) { component.performUpdateIfNecessary(); callback && callback();return;
  }

  dirtyComponents.push(component);

  if (callback) {
    if (component._pendingCallbacks) {
      component._pendingCallbacks.push(callback);
    } else{ component._pendingCallbacks = [callback]; }}}Copy the code

First remind “batch Update policy object” must be injected, otherwise give a warning. React batch update strategy has a default in the implementation of the object, the implementation code is in ReactDefaultBatchingStrategy js file, it like this:

var ReactDefaultBatchingStrategy = {
  isBatchingUpdates: false,

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

    ReactDefaultBatchingStrategy.isBatchingUpdates = true;

    // The code is written this way to avoid extra allocations
    if(alreadyBatchingUpdates) {// alreadyBatchingUpdatestrueCallback (Param); }else{ transaction.perform(callback, null, param); }}};Copy the code

Where does the injection take place? Dependency injection in React is done in reactDefaultinjection.js. This file is also mentioned in The Deep Dive into the React Synthetic Event System. Here, I’m just picking up code that is relevant to the topic of this article:

 ReactUpdates.injection.injectBatchingStrategy(
    ReactDefaultBatchingStrategy
  );
Copy the code

Let’s now return to the discussion of the source code for the enqueueUpdate method. If the isBatchingUpdates bit of the batchingStrategy object is false, then we can directly enter the interface update process:

if(! batchingStrategy.isBatchingUpdates) { component.performUpdateIfNecessary(); callback && callback();return;
  }
Copy the code

Otherwise, react places the component instance that currently calls the setState method in the dirtyComponents and stores the setState callback function in the component instance’s _pendingCallbacks field:

dirtyComponents.push(component);

  if (callback) {
    if (component._pendingCallbacks) {
      component._pendingCallbacks.push(callback);
    } else{ component._pendingCallbacks = [callback]; }}Copy the code

In fact, if we write the code as if… else… To see where the code is going:

if(! batchingStrategy.isBatchingUpdates) { component.performUpdateIfNecessary(); callback && callback(); }else {
    dirtyComponents.push(component);

    if (callback) {
        if (component._pendingCallbacks) {
          component._pendingCallbacks.push(callback);
        } else{ component._pendingCallbacks = [callback]; }}}Copy the code

Before I go any further, let me talk about what dirtyComponents really are.

First, it is an array in terms of data structure:

var dirtyComponents = [];
Copy the code

Second, we need to be clear about what we mean by “dirty component.”

/**
 * Mark a component as needing a rerender, adding an optional callback to a
 * list of functions which will be executed once the rerender occurs.
 */
Copy the code

With the comments from the source code above and my own research, I can define a dirty component:

A component is said to be “dirty” if rerender cannot be rerender in a synchronized manner.

The concept of dirty components might be better understood if we could look at the method name “setState” in a different way. In fact, in my understanding, “setState” should not be called setState, but should be called “requestToSetState”. When the user calls setState, it’s essentially asking React to update the interface immediately. SetState semantically requests updates, but essentially it also does one thing, that is, shallow merge the passed partialState with the original state, and finally assign the value to the component instance’s _pendingState. React will not immediately respond to our update request, and the component is in the “_pendingState” state. In this case, foreigners call it “dirty”. We Chinese should also understand.

So, why didn’t React immediately agree to our update request? The answer to this question can be found in the source code for the enqueueUpdate method given above. That is to see if the batch update flag bit is true.

“IsBatchingUpdates”, which literally means “whether or not you are in batch updates”, will not be covered here. Then we ask ourselves, “When is the value of isBatchingUpdates true?”

We might as well the global search, after a fierce operations will find the “isBatchingUpdates” assignment statements are in ReactDefaultBatchingStrategy. Js file:

The first place is:

var ReactDefaultBatchingStrategy = {
  isBatchingUpdates: false, / /... };Copy the code

Second place is also in this ReactDefaultBatchingStrategy object is specific in its batchedUpdates method definitions inside:

  batchedUpdates: function(callback, param) {
    // ......
    ReactDefaultBatchingStrategy.isBatchingUpdates = true; / /... }};Copy the code

Third place is in ReactDefaultBatchingStrategyTransaction inside a wrapper called RESET_BATCHED_UPDATES close method:

var RESET_BATCHED_UPDATES = {
  // ......
  close: function() {
    ReactDefaultBatchingStrategy.isBatchingUpdates = false; }};Copy the code

Given the above search results and our understanding of the transaction pattern, we can see that the assignment flow for “isBatchingUpdates” looks like this:

  1. In the initialization ReactDefaultBatchingStrategy object, give a default value or false;
  2. When a third party calls the batchedUpdates method, set the value to true;
  3. At the end of the ReactDefaultBatchingStrategyTransaction, assignment is false;

So who called the batchedUpdates method from where? After a global search, we found only two calls to the batchedUpdates method:

// In reactupdates.jsfunction batchedUpdates(callback, param) {
  ensureBatchingStrategy();
  batchingStrategy.batchedUpdates(callback, param);
}
Copy the code
// In reactupdates. js handleTopLevel:function(
      topLevelType,
      topLevelTarget,
      topLevelTargetID,
      nativeEvent) {

    var events = EventPluginHub.extractEvents(
      topLevelType,
      topLevelTarget,
      topLevelTargetID,
      nativeEvent
    );

    ReactUpdates.batchedUpdates(runEventQueueInBatch, events);
  }
Copy the code

And through the code to trace, we found the ReactUpdates handleTopLevel method inside. BatchedUpdates () call is the real call entrance. Because the actual reference-passing process looks like this (” A -> B “means” A passes its own reference to B “) :

ReactDefaultBatchingStrategy.batchedUpdates -> batchingStrategy.batchedUpdates  ->  
ReactUpdates.batchedUpdates
Copy the code

BatchingStrategy and ReactDefaultBatchingStrategy transfer is done through dependency injection mentioned before:

 ReactUpdates.injection.injectBatchingStrategy(
    ReactDefaultBatchingStrategy
  );
Copy the code

The handleTopLevel method is an old friend. I also mentioned it in Deep Dive into the React Synthetic Event System. Those of you who have read this article probably know that there is a chain of calls: topLevelCallback() -> handleTopLevel() -> Event Listener (); And our setState call is made in the Event Listener. Today, the full call chain should look like this (” A -> B “means” A calls B “) :

topLevelCallback() ->  
handleTopLevel() -> 
ReactUpdates.batchedUpdates() -> 
runEventQueueInBatch()  -> 
event listener()  -> 
setState()
Copy the code

At this point, you can see why the setState call to the Event Listener is executed asynchronously. Before it is because the execution setState performed ReactUpdates. BatchedUpdates (), and ReactUpdates. BatchedUpdates () call is open the batch update mode.

If the component. PerformUpdateIfNecessary (); After the call, the interface update process is followed. So when does a component put in the dirtyComponents array enter this process? In the source code, the answer to this question is somewhat hidden. If you have a deeper understanding of transaction, you may be able to find out more quickly. Because, the answer is in the transaction, is a more specific order is ReactDefaultBatchingStrategyTransaction transaction. So here’s how I found out.

ReactUpdates. The reference point to ReactDefaultBatchingStrategy batchedUpdates. BatchedUpdates, see has been said above. And ReactDefaultBatchingStrategy. BatchedUpdates implementation code we should review:

batchedUpdates: function batchedUpdates(callback, param) {
    var alreadyBatchingUpdates = ReactDefaultBatchingStrategy.isBatchingUpdates;
    
    ReactDefaultBatchingStrategy.isBatchingUpdates = true;
    
    // The code is written this way to avoid extra allocations
    if (alreadyBatchingUpdates) {
      callback(param);
    } else{ transaction.perform(callback, null, param); }}Copy the code

As one type of event trigger will only lead to a ReactUpdates. BatchedUpdates method calls and ReactDefaultBatchingStrategy. IsBatchingUpdates initial value is false (that is alrea DyBatchingUpdates first read value is false), so in an event loop, the if conditional branch in the batchedUpdates method is not executed at all. The comment above also mentions The reason for this: “The code is written this way to avoid extra allocations.” So, to call the batchedUpdates method, all we need to notice is this statement:

transaction.perform(callback, null, param);
Copy the code

At this point, we see transaction. Through ReactDefaultBatchingStrategy. Js source code, we will know that this transaction is ReactDefaultBatchingStrategyTransaction. That is to say, our event listener is performed for the ReactDefaultBatchingStrategyTransaction affairs. Because the setState call is inside the Event Listener, the setState call is also inside the transaction. The Transaction mode used in React is a sandwich cookie style, with core methods “wrapped” in wrappers, with initialize methods at the top and close methods at the bottom. The runtime model looks something like this:

 *                       wrappers (injected at creation time)
 *                                      +        +
 *                                      |        |
 *                    +-----------------|--------|--------------+
 *                    |                 v        |              |
 *                    |      +---------------+   |              |
 *                    |   +--|    wrapper1   |---|----+         |
 *                    |   |  +---------------+   v    |         |
 *                    |   |          +-------------+  |         |
 *                    |   |     +----|   wrapper2  |--------+   |
 *                    |   |     |    +-------------+  |     |   |
 *                    |   |     |                     |     |   |
 *                    |   v     v                     v     v   | wrapper
 *                    | +---+ +---+   +---------+   +---+ +---+ | invariants
 * perform(anyMethod) | |   | |   |   |         |   |   | |   | | maintained
 * +----------------->|-|---|-|---|-->|anyMethod|---|---|-|---|-|-------->
 *                    | |   | |   |   |         |   |   | |   | |
 *                    | |   | |   |   |         |   |   | |   | |
 *                    | |   | |   |   |         |   |   | |   | |
 *                    | +---+ +---+   +---------+   +---+ +---+ |
 *                    |  initialize                    close    |
 *                    +-----------------------------------------+
Copy the code

So specific to ReactDefaultBatchingStrategyTransaction, its runtime model embodies as follows

--------FLUSH_BATCHED_UPDATES.emptyFunction------- | | -----RESET_BATCHED_UPDATES.emptyFunction------ | | | | RunEventQueueInBatch | | -- -- -- -- -- -- -- -- FLUSH_BATCHED_UPDATES. Close -- -- -- -- -- - / / the close method is to point to ReactUpdates. | flushBatchedUpdates method ------- flush_batched_updates. close-------function() {
            ReactDefaultBatchingStrategy.isBatchingUpdates = false; // When the transaction is complete, reset the batch update switch tofalse
          }

Copy the code

The above model is executed from top to bottom. Once the runEventQueueInBatch method is executed, the setState call is executed. Follow execution is FLUSH_BATCHED_UPDATES this wrapper close method: ReactUpdates. FlushBatchedUpdates. And that’s where we’re looking for answers. FlushBatchedUpdates: flushBatchedUpdates: flushBatchedUpdates: flushBatchedUpdates: flushBatchedUpdates: flushBatchedUpdates

// In our old friend reactupdates.jsfunction flushBatchedUpdates() {
  // Run these in separate functionsso the JIT can optimize try { runBatchedUpdates(); } catch (e) { // IE 8 requires catch to use finally. throw e; } finally { clearDirtyComponents(); }}Copy the code

How about the implementation of the runBatchedUpdates method:

function runBatchedUpdates() {
  // Since reconciling a component higher in the owner hierarchy usually (not
  // always -- see shouldComponentUpdate()) will reconcile children, reconcile
  // them before their children by sorting the array.

  dirtyComponents.sort(mountDepthComparator);

  for (var i = 0; i < dirtyComponents.length; i++) {
    // If a component is unmounted before pending changes apply, ignore them
    // TODO: Queue unmounts in the same list to avoid this happening at all
    var component = dirtyComponents[i];
    if (component.isMounted()) {
      // If performUpdateIfNecessary happens to enqueue any new updates, we
      // should not execute the callbacks until the next render happens, so
      // stash the callbacks first
      var callbacks = component._pendingCallbacks;
      component._pendingCallbacks = null;
      component.performUpdateIfNecessary();
      if (callbacks) {
        for (var j = 0; j < callbacks.length; j++) {
          callbacks[j].call(component);
        }
      }
    }
  }
}
Copy the code

Did you see the 24-carat titanium eyes? We saw the familiar figure: dirtyComponents and component performUpdateIfNecessary (); . Yes, this is where we can answer the question we asked earlier.

The first question point is: “Who?” The answer: “The runBatchedUpdates method.”

The second question point is: “When?” Answer yue: “in the end of ReactDefaultBatchingStrategyTransaction, perform FLUSH_BATCHED_UPDATES wrapper when close method”.

At this point, the setState, whether it’s synchronous or asynchronous, will reach its intersection in the process of execution. This intersection is:

component.performUpdateIfNecessary();
Copy the code

As the name of the method implies, from this intersection the following process should be the interface update process. The interface update mechanism will be explained in a separate article, which will not be expanded here.

Four topics

From this intersection, we can sort of figure out “why is setState placed in Event Listener executed asynchronously, and why is setState placed in componentDidMount executed synchronously?” That’s the question. So, looking back from this intersection, what are our problems? Answer, our question is “What happens when you call setState multiple times?” . Let’s explore it.

Multiple calls to setState can be subdivided into two scenarios and two modes to study. Therefore, we will have the following four research topics:

  1. In batch update mode, setState is called multiple times in a row in the same method
  2. In batch update mode, components at different levels call setState during component tree update
  3. In non-batch update mode, setState is called multiple times in a row in the same method
  4. In non-batch update mode, components at different levels call setState during component tree update

Below, each topic is examined in combination with a specific example code.

Topic 1

const Parent = React.createClass({
    getInitialState() {return {
            count: 0
        }
    },
    handleClick(){
        this.setState({
            count: this.state.count +  1
        });
        this.setState({
            count: this.state.count +  1
        });
        this.setState({
            count: this.state.count +  1
        });
    },
    render() {return react.DOM.button({
            onClick: this.handleClick
        },`I have been clicked ${this.state.count} times`)}})Copy the code

The setState calls in the handleClick method above are executed asynchronously because they are in batch update mode. This causal relationship has been clearly stated in the article above, and is repeated here. Here, we’re all trying to figure out what happens after we call setState multiple times.

In batch update mode, events that occur after multiple calls to setState are also mentioned briefly. That is, first do a shallow merge of state objects, update the merge result to the component instance’s _pendingState property, and then push the current component instance into dirtyComponents. For this example, because all three of this.state.count access values are 0, the result of three shallow merges {count: this.state.count + 1} is:

{
    count: 1
}
Copy the code

We then push the instance of the Parent component into the dirtyComponents. Because setState is called once and pushed once, there should be three instances of the same component in the dirtyComponents component. Let’s print out the dirtyComponents:

In ActV0.8.0, the component instance looks like this:

(See, _pendingState is {count: 1} waiting for us.) Ok, now dirtyComponents is ready. It can be carried in ReactDefaultBatchingStrategyTransaction flush out close method. The method responsible for flush is described above:

function flushBatchedUpdates() {
  // Run these in separate functionsso the JIT can optimize try { runBatchedUpdates(); } catch (e) { // IE 8 requires catch to use finally. throw e; } finally { clearDirtyComponents(); }}Copy the code

The runBatchedUpdates method is also available from the runBatchedUpdates method.

function runBatchedUpdates() {
  // Since reconciling a component higher in the owner hierarchy usually (not
  // always -- see shouldComponentUpdate()) will reconcile children, reconcile
  // them before their children by sorting the array.

  dirtyComponents.sort(mountDepthComparator);

  for (var i = 0; i < dirtyComponents.length; i++) {
    // If a component is unmounted before pending changes apply, ignore them
    // TODO: Queue unmounts in the same list to avoid this happening at all
    
    var component = dirtyComponents[i];
    if (component.isMounted()) {
      // If performUpdateIfNecessary happens to enqueue any new updates, we
      // should not execute the callbacks until the next render happens, so
      // stash the callbacks first
      
      var callbacks = component._pendingCallbacks;
      component._pendingCallbacks = null;
      component.performUpdateIfNecessary();
      if (callbacks) {
        for (var j = 0; j < callbacks.length; j++) {
          callbacks[j].call(component);
        }
      }
    }
  }
}
Copy the code

DirtyComponents. Sort (mountDepthComparator); Because it belongs to the next topic. As you can see from the source, all you need to do is walk through the dirtyComponents array, calling their performUpdateIfNecessary methods one by one.

In React, reconciliation is an uninterrupted process that starts with the top component and recurses to the bottom component. I call this process a “one-shot update.”

In fact, “one-shot update” can be divided into four stages:

  1. Request update
  2. Decide whether or not to grant the request
  3. Recursive traversal from parent to child
  4. Real DOM manipulation

From the “IfNecessary” in the performUpdateIfNecessary method name, we can guess that this method belongs to the second stage. How does react decide whether to approve updates?

We can find the answer by following the call stack involved in stage 2 (the order of calls in the call stack is from bottom up) :

ReactCompositeComponent.performUpdateIfNecessary ->
ReactComponent.Mixin.performUpdateIfNecessary ->
ReactCompositeComponent._performUpdateIfNecessary ->
ReactCompositeComponent._performComponentUpdate
Copy the code

If you look closely at the source code along the call stack, you’ll see that React determines whether to approve an update request based on three levels.

The first ReactCompositeComponent. PerformUpdateIfNecessary method:

/ /...if (compositeLifeCycleState === CompositeLifeCycle.MOUNTING || compositeLifeCycleState === CompositeLifeCycle.RECEIVING_PROPS) {
  return; } / /...Copy the code

The second and third are in the reactCompositecomponent._performUpdateIfNecessary method:

if(this._pendingProps == null && this._pendingState == null && ! this._pendingForceUpdate) {return;
}
Copy the code
if(this._pendingForceUpdate || ! this.shouldComponentUpdate || this.shouldComponentUpdate(nextProps, nextState)) { this._pendingForceUpdate =false;
  // Will set `this.props` and `this.state`.
  this._performComponentUpdate(nextProps, nextState, transaction);
} else {
  // If it is not determined that a component should not update, we still want
  // to set props and state.
  this.props = nextProps;
  this.state = nextState;
}
Copy the code

Now let’s go through these judgments one by one.

For the first call to the component. PerformUpdateIfNecessary (); , in this case, because the Parent component is at the top of the update component and is caused by calling setState and update, so its CompositeLifeCycle status value for CompositeLifeCycle. RECEIVING_PROPS. The compositeLifeCycleState state value cannot be MOUNTING because this is a component update and not an initial mount. So, the first pass.

Let’s look at the second level. This. _pendingProps is null for the same reason. This. _pendingState cannot be null. The value of this._pendingState is {count: 1}. Therefore, the second level is also passed.

Move on to level three. Because we are not calling the forceUpdate method to update the interface, this._pendingForceUpdate is false; Because we didn’t mount shouldComponentUpdate! Enclosing shouldComponentUpdate value is true, so we sailed through the third crossing.

After passing three levels, call the this._performComponentUpdate method to formally enter the subsequent interface update process.

To sum up, in this example, the first setState call is a complete “one-shot update.”

Had completed a component tree after the update “mirror” in the end, we will then call the second component. PerformUpdateIfNecessary (). It’s worth pointing out here that the component instance that pushes dirtyComponents three times is actually the same. Don’t believe it? Let’s log in the runBatchedUpdates method:

function runBatchedUpdates() {/ /... console.log(dirtyComponents[0] === dirtyComponents[1], dirtyComponents[1] === dirtyComponents[2]); / /... }Copy the code

Print result:

true true
Copy the code

Thus, on the first setState call, there is an operation to clear _pendingState (in the reactCompositecomponent._performUpdateIfNecessary method) after it successfully passes the second level:

_performUpdateIfNecessary: function(transaction) { // ...... var nextState = this._pendingState || this.state; this._pendingState = null; / /... }Copy the code

And because dirtyComponents three component instance is the same, so, in the second call component. PerformUpdateIfNecessary () of the customs inspection of the second level is not. This._pendingProps == null && this._pendingState == null &&! This. _pendingForceUpdate is true (mainly because this._pendingState is null), so the “one-shot update” process stops at this point by executing the return statement.

By the same token, the third component of the performUpdateIfNecessary () call is the same, will not go into here. To support this, we’ll write a log in the ReactCompositeComponent._performUpdateIfNecessary method:

console.log('into _performUpdateIfNecessary:', this._pendingProps == null && this._pendingState == null && ! this._pendingForceUpdate);Copy the code

For this example code, print:

into _performUpdateIfNecessary: false
into _performUpdateIfNecessary: true
into _performUpdateIfNecessary: true
Copy the code

To sum up, three consecutive setState calls in the event handler are essentially three shallow merging of objects and a “one-shot update” in the component tree, while the update process initiated by the following two setState calls are abandoned in the middle of the process and stopped abruptly. Perhaps this is the more literal meaning of “batch update” in React.

Task 2

const Child = React.createClass({
    getInitialState() {return {
            count: 0
        }
    },
    handleClick(){
        this.setState({
            count: this.state.count +  1
        });
    },
    render() {return react.DOM.button({
            onClick: this.handleClick
        },`I have been clicked ${this.state.count} times`)}}); const Parent = React.createClass({getInitialState() {return {
            count: 0
        }
    },
    handleClick(){
        this.setState({
            count: this.state.count +  1
        });
    },
    render() {return react.DOM.div({
            onClick: this.handleClick
        },Child())
    }
});

Copy the code

In this topic, we will talk about the same component tree, different levels of components call setState multiple times.

We can see that both Parent and Child components have a handleClick event handler. They are all registered in the event bubble phase. Therefore, when a user click event occurs, child. handleClick is called first, and parent. click is called after. As a result, Child component instances are pushed into dirtyComponents and Parent component instances are pushed in:

dirtyComponents.sort(mountDepthComparator)

function runBatchedUpdates() {
  // Since reconciling a component higher inthe owner hierarchy usually (not // always -- see shouldComponentUpdate()) will reconcile children, reconcile // them before their children by sorting the array. dirtyComponents.sort(mountDepthComparator); / /... }Copy the code

This line of code is crucial. By sorting the dirtyComponents, React keeps the reconciliation process from the root of the component tree to the lowest component. It is worth mentioning the component instance “_mountDepth” field on which the sort is based. The value of this field is a number that represents the current component level in the component tree. Numbering starts at the root component and increments layer by layer from base 0. From the screenshot above, we can see that the _mountDepth value of the parent component is 0 and the _mountDepth value of the child component is 2.

Sort ((a,b) => a – b); Sort ((a,b) => b – a)

MountDepthComparator; mountDepthComparator; mountDepthComparator;

function mountDepthComparator(c1, c2) {
  return c1._mountDepth - c2._mountDepth;
}
Copy the code

React sorts the dirtyComponents in ascending order based on the value of _mountDepth. This ensures that the root component is always at the top of the array:

That is, in “batch update mode,” regardless of the order in which components at different levels in the component tree call setState, they are finally rearranged before flush dirtyComponents. So, in this example, React tries to update the Parent component first, and then the Child component. In batch update mode, component updates always update the state of all components first (update _pendingState, to be exact), and then try to make real DOM level updates. About this, I believe that the complete read above people will probably know.

In the example, React tries to update the Parent component first. In contrast to the Parent component in the previous example, the update of the Parent component in this example is also a “one-shot update” because both are in batch update mode and are root components.

Because the Parent component’s “one-shot update” updates all components in the entire tree, the Child component’s “_pendingState” field is also cleared. When flush itself, the Child component cannot pass level 2 because this._pendingState is null.

So far, multiple calls to setState in this example have actually done the same thing as in example 1: a shallow merge of state, updated to _pendingState, followed by a “one-shot update”, and all initiated updates end at level 2.

Now, let’s make the research a little harder. How do I augment it? Add a life cycle in the Child component function componentWillReceiveProps:


    const Child = React.createClass({
    getInitialState() {return {
            count: 0
        }
    },
    handleChildClick(){
        this.setState({
            count: this.state.count +  2
        });
    },
    componentWillReceiveProps(){
        this.setState({
            count: 10
        });
    },
    render() {return React.DOM.button({
            onClick: this.handleChildClick
        },`Child count ${this.state.count} times`)}});Copy the code

After joining componentWillReceiveProps, brought three questions:

  1. HandleChildClick and componentWillReceiveProps setState call who who before?
  2. What is the state value of the final Child component?
  3. In this example componentWillReceiveProps calls setState what’s going on?

Let’s answer them one by one.

We start with the user’s click action, and the execution flow looks like this:

  1. Execute handleChildClick to update the _pendingState of the Child component;
  2. Execute handleParentClick to update the Parent component’s _pendingState;
  3. The Parent component in the process of update “mirror” in the end will update Child components, componentWillReceiveProps will call at this time.
  4. Call componentWillReceiveProps (), call setState Child component _pendingState update again.

Answer to question 1:

From the point of this process, is to perform handleChildClick setState first, after the implementation of componentWillReceiveProps setState.

Answer to question 2:

Is responsible for calling from componentWillReceiveProps lifecycle function called ReactCompositeComponent. _performUpdateIfNecessary implementation source code:

 _performUpdateIfNecessary: function(transaction) {
    if(this._pendingProps == null && this._pendingState == null && ! this._pendingForceUpdate) {return;
    }

    var nextProps = this.props;
    if(this._pendingProps ! = null) { nextProps = this._pendingProps; this._processProps(nextProps); this._pendingProps = null; this._compositeLifeCycleState = CompositeLifeCycle.RECEIVING_PROPS;if (this.componentWillReceiveProps) {
        this.componentWillReceiveProps(nextProps, transaction);
      }
    }

    this._compositeLifeCycleState = CompositeLifeCycle.RECEIVING_STATE;

    var nextState = this._pendingState || this.state;
    this._pendingState = null;

    if(this._pendingForceUpdate || ! this.shouldComponentUpdate || this.shouldComponentUpdate(nextProps, nextState)) { this._pendingForceUpdate =false;
      // Will set `this.props` and `this.state`.
      this._performComponentUpdate(nextProps, nextState, transaction);
    } else {
      // If it not determined that a component should not update, we still want
      // to set props and state.
      this.props = nextProps;
      this.state = nextState;
    }

    this._compositeLifeCycleState = null;
  },
Copy the code

We can see that this.com ponentWillReceiveProps (nextProps, transaction); Statement is in var nextState = this. _pendingState | | this. The state; SetState ({count: this.state.count + 1}); setState({count: this.state.count + 1}); 10}), that is, the value of this._pendingState will undergo two shallow object merges to become the nextState used to update the Child component. So, the final state value of the Child component is {count: 10}.

Answer to question 3:

Because componentWillReceiveProps () call is occurred in the Parent component of the process of update “mirror” in the end, and the Parent component “a mirror exactly updates” is also occurs under the “batch update model”, so, ComponentWillReceiveProps setState inside is like handleChildClick setState inside, are executed asynchronously, specific performance is: Merge state and update the combined state value to _pendingState, then push the current component instance inside dirtyComponents. Finally, wait until the current “one-shot update” is complete and flush dirtyComponents work is traversed to you before asking to update yourself. Also, because you can’t get past the second level, it’s doomed to futility.

Without exploring this example, we might intuitively expect the update to happen like this:

Update Child component -> Update Parent component -> Update Child componentCopy the code

After a bit of combing, we found out that it actually happens like this:

Update the _pendingState of the Child component first -> update the _pendingState of the Parent component -> Finally update the Child component in the "One-shot update" process of the Parent componentCopy the code

Topic 3

const Parent = React.createClass({
    getInitialState() {return {
            count: 0
        }
    },
    render() {return React.DOM.button({},`I have been clicked ${this.state.count} times`)},componentDidMount() { this.setState({ count: this.state.count + 1 }); this.setState({ count: this.state.count + 1 }); this.setState({ count: this.state.count + 1 }); }})Copy the code

After studying the above two topics, we won’t have to expound much on what happens with the multiple calls to setState in this example. Because the current setState call is under “batch update model”, so, three setState is essentially the equivalent of “component. PerformUpdateIfNecessary ()” call. Since no other child component requests updates along the way, this means that all three Parent components will be updated in one shot. To verify this, log componentDidUpdate:

componentDidUpdate(){
    console.log('into componentDidUpdate');
}
Copy the code

If you can see, there were three component updates.

Topic 4

    const Child = React.createClass({
    getInitialState() {return {
            count: 0
        }
    },
    componentWillReceiveProps(){
        this.setState({
            count: 10
        });
    },
    render() {return React.DOM.button({
            onClick: this.handleChildClick
        },`Child count ${this.state.count} times`)}}); const Parent = React.createClass({getInitialState() {return {
            count: 0
        }
    },
    render() {return React.DOM.div({},`I have been clicked ${this.state.count} times`)},componentDidMount() { this.setState({ count: this.state.count + 1 }); }})Copy the code

Obviously, this is not “batch update mode” and all setState calls to the Parent components cause a “one-shot update”. In this example, our focus is to study the Child component in componentWillReceiveProps lifecycle function call setState what happened? Will this result in a “one-shot update” with the Child component as the root component? Now, let’s continue our exploration.

We went to call componentWillReceiveProps function ReactCompositeComponent. _performUpdateIfNecessary method source code to see:

_performUpdateIfNecessary: function(transaction) {
    if(this._pendingProps == null && this._pendingState == null && ! this._pendingForceUpdate) {return;
    }

    var nextProps = this.props;
    if(this._pendingProps ! = null) { nextProps = this._pendingProps; this._processProps(nextProps); this._pendingProps = null; this._compositeLifeCycleState = CompositeLifeCycle.RECEIVING_PROPS;if (this.componentWillReceiveProps) {
        this.componentWillReceiveProps(nextProps, transaction);
      }
    }

    this._compositeLifeCycleState = CompositeLifeCycle.RECEIVING_STATE;

    var nextState = this._pendingState || this.state;
    this._pendingState = null;

    if(this._pendingForceUpdate || ! this.shouldComponentUpdate || this.shouldComponentUpdate(nextProps, nextState)) { this._pendingForceUpdate =false;
      // Will set `this.props` and `this.state`.
      this._performComponentUpdate(nextProps, nextState, transaction);
    } else {
      // If it is not determined that a component should not update, we still want
      // to set props and state.
      this.props = nextProps;
      this.state = nextState;
    }

    this._compositeLifeCycleState = null;
  },
Copy the code

Before we see is componentWillReceiveProps lifecycle function called a to the operation of the component’s life cycle state assignment:

this._compositeLifeCycleState = CompositeLifeCycle.RECEIVING_STATE;
Copy the code

This is an important line of code, so write it down. Because the current is not in “batch update model”, so call setState componentWillReceiveProps, do first _pendingState update, finally back to the intersection we mentioned above: component.performUpdateIfNecessary(); . This is our old friend, mentioned repeatedly in the example above. It is the starting point for the component’s true DOM level update process. From this starting point, there are three levels, also mentioned above.

We might as well from the first level (in ReactCompositeComponent performUpdateIfNecessary method) began to see:

/ /...if (compositeLifeCycleState === CompositeLifeCycle.MOUNTING || compositeLifeCycleState === CompositeLifeCycle.RECEIVING_PROPS) {
  return; } / /...Copy the code

Look at carefully, we found that there is such a condition compositeLifeCycleState. = = = CompositeLifeCycle RECEIVING_PROPS. And upgrade a few lines of text, we mentioned that before calling componentWillReceiveProps, actually already to compositeLifeCycleState fu value, this value is CompositeLifeCycle. RECEIVING_PROPS. So the answer here is obvious. That is, the condition is met, and the return statement is executed. The first level is unpassable, so the answer to the question “would this result in a snap-through update” with the Child component as the root component, which we speculated before exploring this example, is “No”.

conclusion

After some research, we can conclude (for ACTV0.8.0 only) :

  • React has two update modes: batch update mode and non-batch update mode. In batch update mode, setState is executed asynchronously. In non-batch update mode, setState is executed synchronously.
  • The idea of “batch” is that multiple update requests for the same component are consolidated into one update request (that is, one final _pendingState), and multiple component update requests are consolidated into one root component update (that is, only one “one-shot update”).
  • ReactUpdates. BatchedUpdates () call is open the batch update mode.
  • React always ensures that updates are updated from high (layer components) to low (layer components) regardless of the order in which you request updates.
  • React ensures a single “one-shot update” by setting up three levels:
1. Number one:if (compositeLifeCycleState === CompositeLifeCycle.MOUNTING || compositeLifeCycleState === CompositeLifeCycle.RECEIVING_PROPS) {
  return; } 2.if(this._pendingProps == null && this._pendingState == null && ! this._pendingForceUpdate) {return; } 3.if(this._pendingForceUpdate || ! this.shouldComponentUpdate || this.shouldComponentUpdate(nextProps, nextState)) { this._pendingForceUpdate =false;
  this._performComponentUpdate(nextProps, nextState, transaction);
} else {
  // If it is not determined that a component should not update, we still want
  // to set props and state.
  this.props = nextProps;
  this.state = nextState;
}
Copy the code

At this point, I have written 8,820 words, and I believe I have already explored the details of the setState mechanism. I think the first thing to do to understand the essence of the setState mechanism is to understand “setState” as “requestToSetState”. Because of the name, I’ve always felt that the React team called the API “setState” somewhat inappropriately. Perhaps for its short name, who Knows….. .

We also have to think about what is the purpose of introducing this mechanism? Reduce unnecessary component rendering. More specifically, it reduces unnecessary function calls and DOM manipulation, resulting in better performance of interface updates.

Finally, as I mentioned above, we can divide the component update process into four phases:

  1. Request update
  2. Decide whether or not to grant the request
  3. Recursive traversal from parent to child
  4. Real DOM manipulation

The update flow chart is as follows

RCC:ReactCompositeComponent

RDC:ReactDOMComponent

RTC:ReactTextComponent

Obviously, the setState mechanism studied in this paper only covers the first two phases. As for the depth of the latter two stages, wait for the day to write a detailed elaboration.

Thank you for reading.