Is setState synchronous or asynchronous? Many of you have probably been in an interview where you are given a piece of code to say what you want to output, like this:

  constructor(props) {
    super(props);
    this.state = {
      data: 'data'}}componentDidMount() {
    this.setState({
      data: 'did mount state'
    })

    console.log("did mount state ".this.state.data);
    // did mount state data

    setTimeout(() = > {
      this.setState({
        data: 'setTimeout'
      })
  
      console.log("setTimeout ".this.state.data); })}Copy the code

As a result of this code, the first console.log will print data and the second console.log will print setTimeout. So on the first setState, it’s asynchronous, and on the second setState, it’s synchronous. Are you a little dizzy? Don’t panic, let’s go to the source code to see what it does.

conclusion

I’m going to put the conclusion up front, so if you’re too lazy to read it, you can read the conclusion.

As soon as you get into the React scheduling process, it’s asynchronous. As long as you’re not in the React scheduling process, it’s synchronous. What doesn’t make it into the React scheduling process? SetTimeout setInterval, directly binding native events to the DOM, etc. None of this is going to follow the React scheduling process, so if you call setState in this case, it’s going to be synchronized. Otherwise it’s asynchronous.

In the case of setState synchronization, the DOM will also be updated synchronously, which means that if you setState multiple times, it will result in multiple updates, which is meaningless and wasteful of performance.

scheduleUpdateOnFiber

When setState is called, it eventually goes to scheduleUpdateOnFiber, so let’s see what it does:

function scheduleUpdateOnFiber(fiber, expirationTime) {
  checkForNestedUpdates();
  warnAboutRenderPhaseUpdatesInDEV(fiber);
  var root = markUpdateTimeFromFiberToRoot(fiber, expirationTime);

  if (root === null) {
    warnAboutUpdateOnUnmountedFiberInDEV(fiber);
    return;
  }

  checkForInterruption(fiber, expirationTime);
  recordScheduleUpdate(); // TODO: computeExpirationForFiber also reads the priority. Pass the
  // priority as an argument to that function and this one.

  var priorityLevel = getCurrentPriorityLevel();

  if (expirationTime === Sync) {
    if ( // Check if we're inside unbatchedUpdates(executionContext & LegacyUnbatchedContext) ! == NoContext &&// Check if we're not already rendering
    (executionContext & (RenderContext | CommitContext)) === NoContext) {
      // Register pending interactions on the root to avoid losing traced interaction data.
      schedulePendingInteractions(root, expirationTime); // This is a legacy edge case. The initial mount of a ReactDOM.render-ed
      // root inside of batchedUpdates should be synchronous, but layout updates
      // should be deferred until the end of the batch.

      performSyncWorkOnRoot(root);
    } else {
      ensureRootIsScheduled(root);
      schedulePendingInteractions(root, expirationTime);

	  / / the point!!!!!!!!!!!!!!!
      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(); }}}else {
    ensureRootIsScheduled(root);
    schedulePendingInteractions(root, expirationTime);
  }

  if((executionContext & DiscreteEventContext) ! == NoContext && (// Only updates at user-blocking priority or greater are considered
  // discrete, even inside a discrete event.
  priorityLevel === UserBlockingPriority$1 || priorityLevel === ImmediatePriority)) {
    // This is the result of a discrete event. Track the lowest priority
    // discrete update per root so we can flush them early, if needed.
    if (rootsWithPendingDiscreteUpdates === null) {
      rootsWithPendingDiscreteUpdates = new Map([[root, expirationTime]]);
    } else {
      var lastDiscreteTime = rootsWithPendingDiscreteUpdates.get(root);

      if (lastDiscreteTime === undefined|| lastDiscreteTime > expirationTime) { rootsWithPendingDiscreteUpdates.set(root, expirationTime); }}}}Copy the code

Let’s focus on this code:

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();
}
Copy the code

Execute Context represents the current react phase, and NoContext is the state where react has run out of work. In flushSyncCallbackQueue our this.setState will be called synchronically, which means that our state will be updated synchronically. So, we know that when executionContext is NoContext, our setState is synchronous. So where do I change the value of executionContext?

Let’s look at some random places

function batchedEventUpdates$1(fn, a) {
  var prevExecutionContext = executionContext;
  executionContext |= EventContext;
  ...省略
}

function batchedUpdates$1(fn, a) {
  var prevExecutionContext = executionContext;
  executionContext |= BatchedContext;
  ...省略
}
Copy the code

When React goes into its own scheduling process, it assigns different values to this executionContext that represent different operations and the current state, and the initial value of executionContext is NoContext, So as long as you’re not in the react scheduler, this value is NoContext, and your setState is synchronous.

The setState useState

Since raect has hooks, function components can have their own state, so if we call setState it will have the same effect as this.setState.

Yes, because useState’s set function also ends up in scheduleUpdateOnFiber, it’s no different from this.setState in this respect.

However, it is worth noting that when we call this.setState, it will automatically do a state merge for us, whereas hook will not, so we need to pay attention to this when we use it.

An 🌰

state = {
  data: 'data'.data1: 'data1'
};

this.setState({ data: 'new data' });
console.log(state);
//{ data: 'new data',data1: 'data1' }

const [state, setState] = useState({ data: 'data'.data1: 'data1' });
setState({ data: 'new data' });
console.log(state);
//{ data: 'new data' }
Copy the code

But if you try to print state after calling setState in the function component’s setTimeout, you will find that it does not change. Then you will wonder why. Isn’t that synchronous?

Because of a closure problem, you still get the last state, so the printed value is the last one, and the real state has been changed. Is there any other way to observe the synchronous behavior of function? Yes, we’ll talk about that next.

Case analysis

SetTimeout, calling setState from a native event, is rare, but it must be common.

  fetch = async() = > {return new Promise((resolve) = > {
      setTimeout(() = > {
        resolve('fetch data');
      }, 300); })}componentDidMount(){(async() = > {const data = await this.fetch();
      this.setState({data});
      console.log("data: ".this.state);
      // data: fetch data}}) ()Copy the code

We sent a request in didMount and then setState the result, and we are processing with async/await.

And then we’ll see that setState actually becomes synchronization. Why? Because componentDidMount has exited react scheduling, and the code we requested has not been executed, setState will not be executed until the result request comes back. The code behind “await” in async functions is actually executed asynchronously. This is actually the same effect as when we do setState in setTimeout, so our setState becomes synchronous.

What’s the harm if it becomes synchronous? Let’s see what happens if we call setState multiple times.

this.state = {
  data: 'init data',}componentDidMount() {
    setTimeout(() = > {
      this.setState({data: 'data 1'});
      // console.log("dom value", document.querySelector('#state').innerHTML);
      this.setState({data: 'data 2'});
      // console.log("dom value", document.querySelector('#state').innerHTML);
      this.setState({data: 'data 3'});
      // console.log("dom value", document.querySelector('#state').innerHTML);
    }, 1000)}render() {
  return (
    <div id="state">
      {this.state.data}
    </div>
  );
}
Copy the code

This is the result of running in the browser

So if you look at it this way, it doesn’t really matter, because every time you refresh it, it still shows up, rightdata 3, but we put in the codeconsole.logDelete the comment and look again:We can be there every timeDOMGo on and get the lateststateThis is becausereactHave thestateThe changes are updated synchronously, but why is the interface not displayed? Because rendering threads and JS threads are mutually exclusive to the browser,reactThe browser can’t render while the code is running. So we’ve actually takenDOMIt’s updated, butstateIt’s been changed again,reactI had to do another update, and I did this three times, and finallyreactAfter the code is executed, the browser renders the final result to the interface. This means that we’ve already made two useless updates.

If we remove setTimeout, we’ll see that init data is printed all three times, because setState is asynchronous and will merge the three updates into one execution.

So be careful not to write code that updates the React component multiple times when setState becomes synchronous. It makes no sense.

And this answers the question, if you want to observe synchronization in function, you can try to see if the contents of the DOM change when you setState in setTimeout.

conclusion

React has helped us make many optimization measures, but sometimes the performance optimization of React will fail due to different implementation methods of code, which is equivalent to our own anti-optimization. So understanding how React works is really helpful in our daily development.