Click here for the React Principles column

This article was inspired by an article I just read today: Is setState in React a macro task or a micro task? . This article is very good, I recommend you to take a look. So, here IS my own understanding.

The asynchronous phenomenon

Look at a simple example

class App extends Component {
  constructor(props) {
    super(props)
    this.state = {
      a: 1
    }
  }

  handleClick = () = > {
    console.log('start'.this.state.a)
    this.setState(
      {
        a: 2
      },
      () = > {
        console.log('set state'.this.state.a)
      }
    )
    console.log('end'.this.state.a)
  }

  render() {
    return <button onClick={this.handleClick}>click</button>}}Copy the code

The output is start 1 -> end 1 -> set state 2.

This result clearly shows that setState is asynchronous.

Now that it’s clear that setState is asynchronous, let’s explore whether setState is a macro task or a micro task. Look at the code below

class App extends Component {
  constructor(props) {
    super(props)
    this.state = {
      a: 1
    }
  }

  handleClick = () = > {
    console.log('start'.this.state.a)
    // Add the promise and setTimeout code
    Promise.resolve().then(() = > console.log('promise'))
    setTimeout(() = > {
      console.log('set timeout')})this.setState(
      {
        a: 2
      },
      () = > {
        console.log('set state'.this.state.a)
      }
    )
    console.log('end'.this.state.a)
  }

  render() {
    return <button onClick={this.handleClick}>click</button>}}Copy the code

Start 1 -> end 1 -> set state 2 -> Promise -> set timeout

As you can see, the output of the setState callback comes before the promise. What? Could it be that setState internally implements microtasks with higher priority than promise? React also sneakily implemented a new browser API?

See through the appearance to the essence

Obviously, the question raised above is impossible. So what about the setState output? Let’s see how react implements setState.

SetState calls classComponentUpdater’s enqueueSetState method, which calls enqueueUpdate to enqueue an Update object and then scheduleUpdateOnFiber to schedule updates.

ClassComponentUpdater is an update to a class component

The scheduleUpdateOnFiber method source code is as follows (remove unimportant code)

function scheduleUpdateOnFiber(fiber, lane, eventTime) {
  / /...
  if (lane === SyncLane) {
    if( (executionContext & LegacyUnbatchedContext) ! == NoContext && (executionContext & (RenderContext | CommitContext)) === NoContext) { schedulePendingInteractions(root, lane); performSyncWorkOnRoot(root); }else {
      ensureRootIsScheduled(root, eventTime);
      schedulePendingInteractions(root, lane);

      if(executionContext === NoContext) { resetRenderTimer(); flushSyncCallbackQueue(); }}}else {
    / /...
  }
  / /...
}
Copy the code

Since we are not using concurrent mode in React17, lane === SyncLane is valid.

(executionContext & LegacyUnbatchedContext) ! == NoContext &&(executionContext & (RenderContext | CommitContext)) === NoContextCopy the code

When ReactDOM. Render is executed, the executionContext is set to LegacyUnbatchedContext, and when render or commit is executed, ExecutionContext will be set to RenderContext and CommitContext, respectively. Thus, this logic is one that is limited to the initial render, and any subsequent updates will enter the else logic, so the ensureRootIsScheduled method is enforced.

EnsureRootIsScheduled is listed below

function ensureRootIsScheduled(root, currentTime) {
  // ...
  if (newCallbackPriority === SyncLanePriority) {
    newCallbackNode = scheduleSyncCallback(performSyncWorkOnRoot.bind(null, root));
  }
  // ...
}
Copy the code

EnsureRootIsScheduled is listed below

function scheduleSyncCallback(callback) {
  // Push this callback into an internal queue. We'll flush these either in
  // the next tick, or earlier if something calls `flushSyncCallbackQueue`.
  if (syncQueue === null) {
    syncQueue = [callback]; // Flush the queue in the next tick, at the earliest.
    immediateQueueCallbackNode = Scheduler_scheduleCallback(Scheduler_ImmediatePriority, flushSyncCallbackQueueImpl);
  } else {
    syncQueue.push(callback);
  }

  return fakeCallbackNode;
}
Copy the code

One idea that is ensureRootIsScheduled is to schedule the execution of a performSyncWorkOnRoot method with a Scheduler module that provides a scheduleCallback method. For those of you familiar with scheduler, scheduleCallback uses MessageChannel to schedule tasks, which is a macro task. This is true, but in Act17, instead of scheduling tasks this way, tasks are synchronized.

Now recall that there are two general places we call setState: in componentDidMount and in event handlers.

incomponentDidMountIn the callsetStateIn the case

See first ReactDOM render, this method will be called legacyRenderSubtreeIntoContainer, and this method has such a piece of code

unbatchedUpdates(function () {
  updateContainer(children, fiberRoot, parentComponent, callback);
});
Copy the code

Take a look at the implementation of unbatchedUpdates

function unbatchedUpdates(fn, a) {
  var prevExecutionContext = executionContext;
  executionContext &= ~BatchedContext;
  executionContext |= LegacyUnbatchedContext;

  try {
    // fn is updateContainer
    return fn(a);
  } finally {
    executionContext = prevExecutionContext;

    if (executionContext === NoContext) {
      // Flush the immediate callbacks that were scheduled during this batchresetRenderTimer(); flushSyncCallbackQueue(); }}}Copy the code

Our componentDidMount lifecycle hook is called in Fn is updateContainer, and setState is called in componentDidMount. ExecutionContext === NoContext is set. FlushSyncCallbackQueue is used in the flushSyncCallbackQueue. Note this comment in scheduleSyncCallback

// Push this callback into an internal queue. We'll flush these either in
// the next tick, or earlier if something calls `flushSyncCallbackQueue`.
Copy the code

Putting callback(in this case performSyncWorkOnRoot) into an internal queue (in this case syncQueue) will schedule the task in the next tick, Or schedule it earlier when the flushSyncCallbackQueue is called. Yes, the flushSyncCallbackQueue called in unbatchedUpdates.

Notice that it is still in the react synchronization code and has not yet entered the next event loop, so it is indeed earlier

FlushSyncCallbackQueue = flushSyncworkonRoot; scheduleSyncCallback = flushSyncCallbackQueue = performSyncWorkOnRoot; scheduleSyncCallback = flushSyncCallbackQueue = flushSyncWorkonroot; FlushSyncCallbackQueue handles this problem internally

function flushSyncCallbackQueue() {
  / / in scheduleSyncCallback immediateQueueCallbackNode assignment
  if(immediateQueueCallbackNode ! = =null) {
    / / if immediateQueueCallbackNode exists, then cancel the task
    var node = immediateQueueCallbackNode;
    immediateQueueCallbackNode = null;
    Scheduler_cancelCallback(node);
  }
  // Execute the tasks in the queue
  flushSyncCallbackQueueImpl();
}
Copy the code

Now, we know that setState is going to put the updated entry function, performSyncWorkOnRoot, in a task queue, and wait until the code in componentDidMount is finished before executing performSyncWorkOnRoot, The callback for the second argument to setState is executed in performSyncWorkOnRoot, so the order of execution is: code in componentDidMount -> setState callback, so setState looks asynchronous.

Called in an event handler functionsetStateIn the case

As you can see from the previous section, the unbatchedUpdates method is the main reason for setState to execute asynchronously. React also uses the same method for event handlers, such as firing a click event that triggers a dispatchEvent. This method calls batchedEventUpdates$1,

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

  try {
    // fn is the event handler function
    return fn(a);
  } finally {
    executionContext = prevExecutionContext;

    if (executionContext === NoContext) {
      // Flush the immediate callbacks that were scheduled during this batchresetRenderTimer(); flushSyncCallbackQueue(); }}}Copy the code

As you can see, the code here is basically the same as unbatchedUpdates.

summary

When setState is executed, performSyncWorkOnRoot, the function that triggered the update, is put into a queue and then the rest of the code is executed. The setState callback is executed in performSyncWorkOnRoot, so setState looks asynchronous, but in reality setState is still a synchronous task, except that the internal order of execution is not synchronous.

I don’t know if you have such doubts: Scheduler is already used to schedule performSyncWorkOnRoot, so why call flushSyncCallbackQueue in batchedEventUpdates$1 to cancel the scheduled task? And then manually execute performSyncWorkOnRoot. I think this may be preparation for the future Concurrent mode.

Multiple setState?

Now the question is, what if we call multiple setStates, will it trigger multiple updates? Those of you who have used React know that it does not trigger multiple updates. One of the ensureRootIsScheduled methods is one with such code before calling scheduleSyncCallback

if(existingCallbackNode ! = =null) {
  var existingCallbackPriority = root.callbackPriority;

  if (existingCallbackPriority === newCallbackPriority) {
    return;
  }
  cancelCallback(existingCallbackNode);
}
Copy the code

ExistingCallbackNode is a scheduled task. When a scheduled task already exists, if the new task has the same priority as the old task, it is returned directly, and no new task will be scheduled.

Let’s call setState in some other way

Call setState in componentDidMount and event handler

componentDidMount() {
  delay(1000).then(() = > {
    this.setState(
      {
        a: 1
      },
      () = > console.log('set state cb'))console.log('set state1'.this.state.a)
    this.setState(
      {
        a: 2
      },
      () = > console.log('set state cb'))console.log('set state2'.this.state.a)
  })
}
Copy the code

Set state cb -> set state1 1 -> set state CB -> set state2 2 As you can see, in this case, setState becomes “synchronous” again. Why? Look at the scheduleUpdateOnFiber

function scheduleUpdateOnFiber(fiber, lane, eventTime) {
  / /...
  ensureRootIsScheduled(root, eventTime);
  schedulePendingInteractions(root, lane);

  if(executionContext === NoContext) { resetRenderTimer(); flushSyncCallbackQueue(); }}Copy the code

If you call setState with a promise’s then callback, the callback executes itself, executionContext is not given any value, so executionContext is NoContext, FlushSyncCallbackQueue is executed so that every setState is executed an update is immediately started, so the output in the previous example is synchronous. In this case, we can use this API: unstable_batchedUpdates

function batchedUpdates$1(fn, a) {
  var prevExecutionContext = executionContext;
  executionContext |= BatchedContext;

  try {
    return fn(a);
  } finally {
    executionContext = prevExecutionContext;

    if (executionContext === NoContext) {
      // Flush the immediate callbacks that were scheduled during this batchresetRenderTimer(); flushSyncCallbackQueue(); }}}Copy the code

So executionContext is no longer NoContext, scheduleUpdateOnFiber doesn’t start updating ahead of time.

conclusion

From this article, we can see that setState appears to be asynchronous, not using macro tasks or microtasks, but using synchronous code to achieve the effect of asynchronous execution. This is done by delaying the execution of updates through an internal task queue.