The problem

Another bug, not to mention, are tears, here directly post the “problematic code”

import React from "react";

class Parent extends React.Component {
  constructor(props) {
    super(props);
    this.state = {
      data: 1.flag: false}; } onValidateSuccessSubmit =(data) = > {
    this.setState({ flag: true });
    // do something
    this.setState({ data });
  };

  asyncFunc = () = > {
    return true;
  };

  onClick = async() = > {const validate = await this.asyncFunc();
    if (validate) {
      this.onValidateSuccessSubmit(2);
    } else {
      // do something}}; onSubmit =() = > {
    console.log(this.state.data);
  };

  render() {
    return (
      <div onClick={this.onClick}>
        <div>click it</div>
        <ChildComponent flag={this.state.flag} onSubmit={this.onSubmit} />
      </div>); }}export default Parent;

class ChildComponent extends React.Component {
  componentWillReceiveProps(nextProps) {
    if(nextProps.flag) { nextProps.onSubmit(); }}render() {
    return <div>child component</div>; }}Copy the code

A brief explanation of the logic of this code:

  1. Click the parent component to execute the onClick event, which gets a variable with async/await.
  2. Execute the onValidateSuccessSubmit event, which fires setState twice
  3. SetState flag set to true, trigger a subcomponent componentWillReceiveProps hook function, perform the parent component onSubmit function.
  4. The parent component’s onSubmit function outputs the data in state.

The console output will be 1 and 2, so when we use the onSubmit function to process the business logic, we will get the state before the update, and then we will not get 😭

And you can think about why is that? What causes this?

Initial guess

Since the output is twice, and we all know that setState has synchronous and asynchronous, could it be the synchronous asynchronous state of setState that causes it? To test our hypothesis, we changed the key code to look like this:

onClick = () = > {
    const validate = this.asyncFunc();
    if (validate) {
      setTimeout(() = > {
        this.onValidateSuccessSubmit(2);
      }, 0);
    } else {
      // do something}};Copy the code

Sure enough, the output is the same as with async/await. Take a look at the source code to verify that the process is running exactly the same.

Synthesize event setState source code

Let’s look at what react does when we setState (react version 16.12.0).

First look at setState in a normal compositing event, where the key code is as follows:

onClick = () = > {
    const validate = this.asyncFunc();
    if (validate) {
      this.onValidateSuccessSubmit(2);
    } else {
      // do something}};Copy the code

When we execute this.setstate ({flag: true}), react does the following:

Disclaimer: Because this is I through the debugger at the same time based on my react very shallow understanding to write out the article, for react a lot of details processing is not introduced, but also hope that you understand, for which the wrong place more correct.

Perform setState

// packages/react/src/ReactBaseClasses.js
/** * Sets a subset of the state. Always use this 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  to setState is actually * completed. * * When a function is provided to setState, it will be called at some point in * the future (not synchronously). It will be called with the up to date * component arguments (state, props, context). These values can be different * from this.* because your function may be called after receiveProps but before * shouldComponentUpdate, and this new state, props, and context will not yet be * assigned to this. * *@param {object|function} partialState Next partial state or function to
 *        produce next partial state to be merged with current state.
 * @param {? function} callback Called after state is updated.
 * @final
 * @protected* /
Component.prototype.setState = function(partialState, callback) {
  invariant(
    typeof partialState === 'object' ||
      typeof partialState === 'function' ||
      partialState == null.'setState(...) : takes an object of state variables to update or a ' +
      'function which returns an object of state variables.',);this.updater.enqueueSetState(this, partialState, callback, 'setState');
};
Copy the code

People who are interested in React can read the comments for themselves to help them understand react.

SetState function will perform this. Updater. EnqueueSetState (this, partialState, callback, ‘setState); , where this is the current component, partialState is the state we will modify, and callback is the callback after modifying state. In fact, it is also a common function to ensure that the event will be triggered after updating state.

enqueueSetState

EnqueueSetState is a method mounted on classComponentUpdater, as shown below

// packages/react-reconciler/src/ReactFiberClassComponent.js
const classComponentUpdater = {
  isMounted,
  enqueueSetState(inst, payload, callback) {
    const fiber = getInstance(inst);
    const currentTime = requestCurrentTimeForUpdate();
    const suspenseConfig = requestCurrentSuspenseConfig();
    const expirationTime = computeExpirationForFiber(
      currentTime,
      fiber,
      suspenseConfig,
    );

    const update = createUpdate(expirationTime, suspenseConfig);
    update.payload = payload;
    if(callback ! = =undefined&& callback ! = =null) {
      if (__DEV__) {
        warnOnInvalidCallback(callback, 'setState'); } update.callback = callback; } enqueueUpdate(fiber, update); scheduleWork(fiber, expirationTime); },... }Copy the code

Let’s focus on the attribute assignment part

const expirationTime = computeExpirationForFiber(
  currentTime,
  fiber,
  suspenseConfig,
);
Copy the code

This function returns a different expirationTime based on the current React mode. React legacy, Blocking, and Concurrent modes use concurrent mode (experimental)

// packages/react-reconciler/src/ReactFiberWorkLoop.js
export function computeExpirationForFiber(
  currentTime: ExpirationTime,
  fiber: Fiber,
  suspenseConfig: null | SuspenseConfig,
) :ExpirationTime {
  const mode = fiber.mode;
  if ((mode & BlockingMode) === NoMode) {
    returnSync; }...return expirationTime;
}
Copy the code

Let’s look at the execution part of the function, enqueueUpdate(fiber, update)

This function takes two arguments, Fiber, which corresponds to the current instance, and update, which we can see is an update object created and returned by the createUpdate function

// packages/react-reconciler/src/ReactUpdateQueue.js
export function createUpdate(
  expirationTime: ExpirationTime,
  suspenseConfig: null | SuspenseConfig,
) :Update< * >{
  let update: Update<*> = {
    expirationTime,
    suspenseConfig,

    tag: UpdateState,
    payload: null.callback: null.next: null.nextEffect: null};if (__DEV__) {
    update.priority = getCurrentPriorityLevel();
  }
  return update;
}
Copy the code

enqueueUpdate

This step is basically to add update Value to the current fiber

// packages/react-reconciler/src/ReactUpdateQueue.js
export function enqueueUpdate<State> (fiber: Fiber, update: Update<State>) {
  // Update queues are created lazily.
  const alternate = fiber.alternate;
  Queue1 and queue2 are fiber pairs
  Queue1 is the current queue
  // Queue2 is the work-in-progress queue
  // For those interested, take a look at the comments at the top of this file and see how the Fiber architecture works in the reference link
  let queue1;
  let queue2;
  if (alternate === null) {
    // There's only one fiber.
    queue1 = fiber.updateQueue;
    queue2 = null;
    if (queue1 === null) {
      // When setState is first executed, fiber's task queues are all null, execute the following code
      // createUpdateQueue this function is used to create an update queue. The parameter fiber.memoizedState is the initial value of this.state.queue1 = fiber.updateQueue = createUpdateQueue(fiber.memoizedState); }}else {
    // There are two owners.
    queue1 = fiber.updateQueue;
    queue2 = alternate.updateQueue;
    if (queue1 === null) {
      if (queue2 === null) {
        // Neither fiber has an update queue. Create new ones.
        queue1 = fiber.updateQueue = createUpdateQueue(fiber.memoizedState);
        queue2 = alternate.updateQueue = createUpdateQueue(
          alternate.memoizedState,
        );
      } else {
        // Only one fiber has an update queue. Clone to create a new one.queue1 = fiber.updateQueue = cloneUpdateQueue(queue2); }}else {
      if (queue2 === null) {
        // Only one fiber has an update queue. Clone to create a new one.
        queue2 = alternate.updateQueue = cloneUpdateQueue(queue1);
      } else {
        // Both owners have an update queue.}}}if (queue2 === null || queue1 === queue2) {
    // There's only a single queue.
    // Then run the following code to add the objects to be updated to the first queue
    appendUpdateToQueue(queue1, update);
  } else {
    // There are two queues. We need to append the update to both queues,
    // While accounting for the persistent structure of the list -- we don't
    // want the same update to be added multiple times.
    if (queue1.lastUpdate === null || queue2.lastUpdate === null) {
      // One of the queues is not empty. We must add the update to both queues.
      appendUpdateToQueue(queue1, update);
      appendUpdateToQueue(queue2, update);
    } else {
      // Both queues are non-empty. The last update is the same in both lists,
      // because of structural sharing. So, only append to one of the lists.
      appendUpdateToQueue(queue1, update);
      // But we still need to update the `lastUpdate` pointer of queue2.queue2.lastUpdate = update; }}if (__DEV__) {
    if( fiber.tag === ClassComponent && (currentlyProcessingQueue === queue1 || (queue2 ! = =null&& currentlyProcessingQueue === queue2)) && ! didWarnUpdateInsideUpdate ) { warningWithoutStack(false.'An update (setState, replaceState, or forceUpdate) was scheduled ' +
          'from inside an update function. Update functions should be pure, ' +
          'with zero side-effects. Consider using componentDidUpdate or a ' +
          'callback.',); didWarnUpdateInsideUpdate =true; }}}Copy the code

Focus on the following two pieces of code:

. queue1 = fiber.updateQueue = createUpdateQueue(fiber.memoizedState); . appendUpdateToQueue(queue1, update); .Copy the code

Fiber updateQueue (firstUpdate, lastUpdate)

  updateQueue: {
    baseState: { a: 1.flag: false },
    firstCapturedEffect: null.firstCapturedUpdate: null.firstEffect: null.firstUpdate: {
      callback: null.expirationTime: 1073741823.next: null.nextEffect: null.payload: { flag: true },
      priority: 98.suspenseConfig: null.tag: 0,},lastCapturedEffect: null.lastCapturedUpdate: null.lastEffect: null.lastUpdate: {
      callback: null.expirationTime: 1073741823.next: null.nextEffect: null.payload: { flag: true },
      priority: 98.suspenseConfig: null.tag: 0,}},Copy the code

The scheduleWork (key

This is where the dispatch phase starts

// packages/react-reconciler/src/ReactFiberWorkLoop.js
export function scheduleUpdateOnFiber(fiber: Fiber, expirationTime: ExpirationTime,) {
  // Check if you have fallen into an infinite loop
  checkForNestedUpdates();
  // Warn in the dev environment, skipped
  warnAboutInvalidUpdatesOnClassComponentsInDEV(fiber);

  // The name indicates the update time from fiber to root. Inside the function there are two main things to do
  Context fiber. ExpirationTime is set to a larger expirationTime, and the larger the expirationTime is, the higher the priority
  // Parent of recursive fiber and set childExpirationTime to expirationTime as well
  // React is an event mechanism.
  const 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.
  const 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);
      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); }... }export const scheduleWork = scheduleUpdateOnFiber;
Copy the code

Direct look at the key code logic part, through the above enqueueSetState attribute assignment as we know, expirationTime assigned to Sync constants, so in here

if (
  // Check if we're inside unbatchedUpdates(executionContext & LegacyUnbatchedContext) ! == NoContext &&// Check if we're not already rendering
  (executionContext & (RenderContext | CommitContext)) === NoContext
)
Copy the code

React batch state: a bunch of binary variables plus bitwise operators in the react batch state file

const NoContext = / * * / 0b000000;
const BatchedContext = / * * / 0b000001;
const EventContext = / * * / 0b000010;
const DiscreteEventContext = / * * / 0b000100;
const LegacyUnbatchedContext = / * * / 0b001000;
const RenderContext = / * * / 0b010000;
const CommitContext = / * * / 0b100000; .// Describes where we are in the React execution stack
let executionContext: ExecutionContext = NoContext;
Copy the code

The executionContext value is number6, the LegacyUnbatchedContext value is number0, and the NoContext value is number0. ExecutionContext is a variable that you’re going to remember, which is the position on the react stack, but we’ll talk about why it’s 6 later. Enter the judgment logic, condition (executionContext & LegacyUnbatchedContext)! == NoContext doesn’t match, else,

ensureRootIsScheduled

// packages/react-reconciler/src/ReactFiberWorkLoop.js
function ensureRootIsScheduled(root: FiberRoot) {...constexistingCallbackNode = root.callbackNode; .// If there's an existing render task, confirm it has the correct priority and
  // expiration time. Otherwise, we'll cancel it and schedule a new one.
  if(existingCallbackNode ! = =null) {
    const existingCallbackPriority = root.callbackPriority;
    const existingCallbackExpirationTime = root.callbackExpirationTime;
    if (
      // Callback must have the exact same expiration time.
      existingCallbackExpirationTime === expirationTime &&
      // Callback must have greater or equal priority.
      existingCallbackPriority >= priorityLevel
    ) {
      // Existing callback is sufficient.
      return;
    }
    // Need to schedule a new task.
    // TODO: Instead of scheduling a new task, we should be able to change the
    // priority of the existing one.cancelCallback(existingCallbackNode); }...let callbackNode;
  if (expirationTime === Sync) {
    // Sync React callbacks are scheduled on a special internal queue
    callbackNode = scheduleSyncCallback(performSyncWorkOnRoot.bind(null, root)); }... root.callbackNode = callbackNode; }Copy the code

=== Sync === === === === === === === === === === === === === === === PerformSyncWorkOnRoot. Bind (null, root) as parameters into the scheduleSyncCallback

scheduleSyncCallback

// packages/react-reconciler/src/SchedulerWithReactIntegration.js
constfakeCallbackNode = {}; .let syncQueue: Array<SchedulerCallback> | null = null; .export function scheduleSyncCallback(callback: SchedulerCallback) {
  // 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 {
    // Push onto existing queue. Don't need to schedule a callback because
    // we already scheduled one when we created the queue.
    syncQueue.push(callback);
  }
  return fakeCallbackNode;
}
Copy the code

SyncQueue global variable is an array type, the initial value is null, and the performSyncWorkOnRoot. Bind (null, root) to assign a value into, immediateQueueCallbackNode does not affect the process will not enter the discussion, Finally, return fakeCallbackNode. FakeCallbackNode is not handled internally, so it returns an empty object. The returned empty object is assigned to root.callbacknode.

schedulePendingInteractions

// Register pending interactions on the root to avoid losing traced interaction data.
schedulePendingInteractions(root, expirationTime);
Copy the code

As you can see from the comments, this function mainly traces and does not affect the process. After this task is complete, the next logical check executionContext === NoContext is executed. If the conditions are inconsistent, the scheduleWork task is terminated.

SetState ({flag: React pushes the SchedulerCallback into an internal queue. There is no diff operation and no render logic. SetState does not trigger the component’s rendering every time.

The subsequent this.setState({data}) procedure should return directly with ensureRootIsScheduled because callbackNode already exists under root.

Due to space problems, the subsequent process and rendering view process will not be discussed, interested students can study by themselves.

SetTimeout setState source code

The key codes are as follows:

onClick = () = > {
    const validate = this.asyncFunc();
    if (validate) {
      setTimeout(() = > {
        this.onValidateSuccessSubmit(2);
      }, 0);
    } else {
      // do something}};Copy the code

SetTimeout setState is similar to the synthetic event except that the value of executionContext in scheduleWork changes to number 0, so flushSyncCallbackQueue is executed. The difference between the execution of a synthesized event and that of a setTimeout is executionContext and flushSyncCallbackQueue. Let’s look at what flushSyncCallbackQueue does.

flushSyncCallbackQueue

export function flushSyncCallbackQueue() {
  if(immediateQueueCallbackNode ! = =null) {
    const node = immediateQueueCallbackNode;
    immediateQueueCallbackNode = null;
    Scheduler_cancelCallback(node);
  }
  flushSyncCallbackQueueImpl();
}
Copy the code

flushSyncCallbackQueueImpl

function flushSyncCallbackQueueImpl() {
  if(! isFlushingSyncQueue && syncQueue ! = =null) {
    // Prevent re-entrancy.
    isFlushingSyncQueue = true;
    let i = 0;
    try {
      const isSync = true;
      const queue = syncQueue;
      runWithPriority(ImmediatePriority, () = > {
        for (; i < queue.length; i++) {
          let callback = queue[i];
          do {
            callback = callback(isSync);
          } while(callback ! = =null); }}); syncQueue =null;
    } catch (error) {
      // If something throws, leave the remaining callbacks on the queue.
      if(syncQueue ! = =null) {
        syncQueue = syncQueue.slice(i + 1);
      }
      // Resume flushing in the next tick
      Scheduler_scheduleCallback(
        Scheduler_ImmediatePriority,
        flushSyncCallbackQueue,
      );
      throw error;
    } finally {
      isFlushingSyncQueue = false; }}}Copy the code

In this method we can clearly see the try block, which takes the previous syncQueue task queue and starts executing the tasks according to their priority.

executionContext

In the procedure of setState above, we did not find that the variable changed. After checking relevant data, we found that React changed the variable when it processed the synthesized event, that is, setState handled the synthesized event before. Let’s take a look at the call stack when the synthesized event was clicked

React handles composited events from dispatchDiscreteEvent to callCallback. After some investigation, we finally find out.

discreteUpdates$1

function discreteUpdates$1(fn, a, b, c) {
  var prevExecutionContext = executionContext;
  executionContext |= DiscreteEventContext;
  try {
    // Should this
    return runWithPriority$2(UserBlockingPriority$2, fn.bind(null, a, b, c));
  } finally {
    executionContext = prevExecutionContext;
    if (executionContext === NoContext) {
      // Flush the immediate callbacks that were scheduled during this batchflushSyncCallbackQueue(); }}}Copy the code

— an optional executionContext | = DiscreteEventContext, about an operator do not know can consult, go here, and here after a bitwise or assigned to — an optional executionContext, The value of the executionContext variable is 0b000100, which is 4 in decimal.

So if you look at the finally block, the prevExecutionContext comes in with a value of 0b000000, and then the prevExecutionContext is assigned to executionContext after the try block is done

DiscreteEventContext is a global variable. The default value is 0b000100. The onClick event of synthesis is DiscreteEvent, event type can reference the react on the react | 1. React in the event delegation

batchedEventUpdates$1

function batchedEventUpdates$1(fn, a) {
  var prevExecutionContext = executionContext;
  executionContext |= EventContext;
  try {
    return fn(a);
  } finally {
    executionContext = prevExecutionContext;
    if (executionContext === NoContext) {
      // Flush the immediate callbacks that were scheduled during this batchflushSyncCallbackQueue(); }}}Copy the code

— an optional executionContext | = EventContext, here again after a bitwise or assigned to — an optional executionContext, at this point — an optional executionContext b00110 variable value is 0, that is 6 in the decimal system.

Afterword.

React handles compositing events, Fiber mechanism, Concurrent mode, render view, etc. Fill in the hole later

Refer to the link

  • Do you really understand setState?
  • Understand JavaScript async/await
  • Why does Async Await work with React setState?
  • How the Fiber architecture works