This is the fifth day of my participation in Gwen Challenge

reacthooks-useState

Source code: github.com/facebook/re…

export function useState<S> (
  initialState: (() => S) | S,
) :S.Dispatch<BasicStateAction<S> >]{
  const dispatcher = resolveDispatcher();
  return dispatcher.useState(initialState);
}

export function useReducer<S.I.A> (reducer: (S, A) => S, initialArg: I, init? : I => S,) :S.Dispatch<A>] {
  const dispatcher = resolveDispatcher();
  return dispatcher.useReducer(reducer, initialArg, init);
}
Copy the code

resolveDispatcher

function resolveDispatcher() {
  //import ReactCurrentDispatcher from './ReactCurrentDispatcher';
  const dispatcher = ReactCurrentDispatcher.current;
  // Fixed format errors are thrown if there is no Dispatcher
  // where %s is allowed as a placeholder for variablesinvariant( dispatcher ! = =null.'Invalid hook call. Hooks can only be called inside of the body of a function component. This could happen for' +
      ' one of the following reasons:\n' +
      '1. You might have mismatching versions of React and the renderer (such as React DOM)\n' +
      '2. You might be breaking the Rules of Hooks\n' +
      '3. You might have more than one copy of React in the same app\n' +
      'See https://fb.me/react-invalid-hook-call for tips about how to debug and fix this problem.',);return dispatcher;
}
Copy the code

ReactCurrentDispatcher

Source code: github.com/facebook/re…

// Import the Dispatcher type
import type {Dispatcher} from 'react-reconciler/src/ReactFiberHooks';
// Trace the current dispatcher.
const ReactCurrentDispatcher = {
  current: (null: null | Dispatcher),
};
export default ReactCurrentDispatcher;
Copy the code

type Dispatcher

Source code: github.com/facebook/re…

useState

Search for useState in type Dispatcher, and if you look down, you’ll find mountState and updateState

mountState

function mountState<S> (
  initialState: (() => S) | S,
) :S.Dispatch<BasicStateAction<S> >]{
  // This function generates a new hook node and returns it at the end of the list
  const hook = mountWorkInProgressHook();
Copy the code

mountWorkInProgressHook

Function: Obtain the current hook node, and add the current hook to the list to form a linked list structure

function mountWorkInProgressHook() :Hook {
  const hook: Hook = {
    Current state [this is the first parameter returned by the reducer update], which stores the updated state
    memoizedState: null.// The state value since the last update, which is related to the priority, is the argument passed to the function when it is executed
    baseState: null.// The Update structure generated from the last Update is used to find the first Update node to be processed in rerender (baseupdate.next)
    baseUpdate: null.// Update the queue
    queue: null.// Next hook
    next: null};// This list stores not only USestate but also useEffect hooks
  if (workInProgressHook === null) {
    // as the head of the list
    / / as you can see the whole chain table stored in the currentlyRenderingFiber. MemoizedState
    currentlyRenderingFiber.memoizedState = workInProgressHook = hook;
  } else {
    // Put the workInProgressHook at the end of the list and point it to the latest node
    workInProgressHook = workInProgressHook.next = hook;
  }
  return workInProgressHook;
}
Copy the code

Back to the code for continuing with mouteState:

  // The code you saw earlier
  const hook = mountWorkInProgressHook();
  // Start from here
  // If it is a function, execute to get the initial state value
  if (typeof initialState === 'function') {
    initialState = initialState();
  }
  // Store the state value
  hook.memoizedState = hook.baseState = initialState;
  // Create a queue containing the set update function
  const queue = (hook.queue = {...
Copy the code

On the queue

Function: Stores the set update function

  // Create a queue containing the set update function
  const queue = (hook.queue = {
    // Last update to be executed (update object, given below)
    pending: null./ / setxxx method
    dispatch: null.Reducer was used when the last rendering was done
    lastRenderedReducer: basicStateReducer,
    // State was used for the last rendering
    lastRenderedState: (initialState: any),
  });
  const dispatch: Dispatch<
    BasicStateAction<S>,
  > = (queue.dispatch = (dispatchAction.bind(
    null,
    currentlyRenderingFiber,
    queue,
  ): any));
  return [hook.memoizedState, dispatch];
}
Copy the code

dispatchAction

When setxxx is used, a new update object is created and stored in the queue. And the update object is a circular linked list.

Other functions:

  1. When rerendering is found, the mapping between queue and update will be established, and the rerendering stage will be taken out for execution.
  2. Quick action: use the current action to calculate a state and save it. If the state is the same as the last one, don’t render it. If the reducer has not changed before the rendering stage, state can be directly used without further calculation.
function dispatchAction<S.A> (fiber: Fiber, queue: UpdateQueue
       
        , action: A,
       ,>) {
  // If there are too many times to rerender, or if the argument is wrong, throw this code
  // ...
  const alternate = fiber.alternate;
  // If it is an update in the render phase,
  if( fiber === currentlyRenderingFiber || (alternate ! = =null && alternate === currentlyRenderingFiber)
  ) {
    // This is a render phase update. Stash it in a lazily-created map of
    // queue -> linked list of updates. After this render pass, we'll restart
    // and apply the stashed updates on top of the work-in-progress hook.
    // This is an update to the render phase, which means a re-render has taken place. The queue and Update mappings are placed in a mapping table, and after the rendering, the updates are taken out and iterated during the rerendering phase.
    didScheduleRenderPhaseUpdate = true;
    // Create an update object for the current update
    const update: Update<S, A> = {
      expirationTime: renderExpirationTime,
      suspenseConfig: null,
      action,
      eagerReducer: null.eagerState: null.next: null};// Get the update priority. The source code is given below
    if (__DEV__) {
      update.priority = getCurrentPriorityLevel();
    }
    // If there is no renderPhaseUpdates mapping, create a new one to store the mapping between the current queue node and the update node, to be used during the rerender phase
    if (renderPhaseUpdates === null) {
      renderPhaseUpdates = new Map(a); }const firstRenderPhaseUpdate = renderPhaseUpdates.get(queue);
    // If there is no update to the corresponding queue, create the corresponding mapping
    if (firstRenderPhaseUpdate === undefined) {
      renderPhaseUpdates.set(queue, update);
    } else {
      // Append the update to the end of the list.
      // Add the update to the end of the list
      let lastRenderPhaseUpdate = firstRenderPhaseUpdate;
      while(lastRenderPhaseUpdate.next ! = =null) { lastRenderPhaseUpdate = lastRenderPhaseUpdate.next; } lastRenderPhaseUpdate.next = update; }}else {
    // Get the current time
    const currentTime = requestCurrentTime();
    const suspenseConfig = requestCurrentSuspenseConfig();
    // Get the expiration time
    const expirationTime = computeExpirationForFiber(
      currentTime,
      fiber,
      suspenseConfig,
    );
    // Create an update object for the current update
    const update: Update<S, A> = {
      expirationTime,
      suspenseConfig,
      action,
      eagerReducer: null.eagerState: null.next: null};// Get the update priority
    if (__DEV__) {
      update.priority = getCurrentPriorityLevel();
    }

    // Append the update to the end of the list.
    // Add the update node to the bottom of the list
    const last = queue.last;
    // If there was no update in the last update to the queue, the current update is the first node
    if (last === null) {
      // This is the first update. Create a circular list.
      // Create a circular linked list by pointing the next node at yourself
      update.next = update;
    } else {
      // If there is an update, get the header
      const first = last.next;
      if(first ! = =null) {
        // Still circular.
        // If there is a header, point the next update node to the header
        update.next = first;
      }
      // use update as the next node of lastupdate to form a circular list
      last.next = update;
    }
    // Update the list of last updates in the queue
    queue.last = update;

    if (
      fiber.expirationTime === NoWork &&
      (alternate === null || alternate.expirationTime === NoWork)
    ) {
      // The queue is currently empty, which means we can eagerly compute the
      // next state before entering the render phase. If the new state is the
      // same as the current state, we may be able to bail out entirely.
      // Queue is currently empty, so we can calculate the next state before entering the render phase. If the next state is the same as the current state, we can exit.
      const lastRenderedReducer = queue.lastRenderedReducer;
      if(lastRenderedReducer ! = =null) {
        let prevDispatcher;
        if (__DEV__) {
          prevDispatcher = ReactCurrentDispatcher.current;
          ReactCurrentDispatcher.current = InvalidNestedHooksDispatcherOnUpdateInDEV;
        }
        try {
          const currentState: S = (queue.lastRenderedState: any);
          const eagerState = lastRenderedReducer(currentState, action);
          // Stash the eagerly computed state, and the reducer used to compute
          // it, on the update object. If the reducer hasn't changed by the
          // time we enter the render phase, then the eager state can be used
          // without calling the reducer again.
          Save the last reducer and the state calculated with it. If the reducer has not changed before the rendering stage, the state can be directly used without calling the reducer to calculate again
          update.eagerReducer = lastRenderedReducer;
          update.eagerState = eagerState;
          if (is(eagerState, currentState)) {
            // Fast path. We can bail out without scheduling React to re-render.
            // It's still possible that we'll need to rebase this update later,
            // if the component re-renders for a different reason and by that
            // time the reducer has changed.
            // Quick path (no need to re-render). If the component needs to be re-rendered for different reasons, the Reducer has been changed and we may have to re-create the update later
            return; }}catch (error) {
          // Suppress the error. It will throw again in the render phase.
        } finally {
          if(__DEV__) { ReactCurrentDispatcher.current = prevDispatcher; }}}}/ /... Omit some warning messages
    // Call scheduleUpdateOnFiber to schedule the taskscheduleWork(fiber, expirationTime); }}Copy the code

updateState

function updateState<S> (
  initialState: (() => S) | S,
) :S.Dispatch<BasicStateAction<S> >]{
  / / return updateReducer
  return updateReducer(basicStateReducer, (initialState: any));
}
Copy the code

basicStateReducer

Action: Calculates action.

function basicStateReducer<S> (state: S, action: BasicStateAction<S>) :S {
  return typeof action === 'function' ? action(state) : action;
}
Copy the code

If setxxx(()=>{}) passes in the state of the last basicStateReducer, then execute the basicStateReducer.

updateReducer

function updateReducer<S.I.A> (reducer: (S, A) => S, initialArg: I, init? : I => S,) :S.Dispatch<A>] {
   // Get the executing Hook node in the update phase
  const hook = updateWorkInProgressHook();
Copy the code

updateWorkInProgressHook

Action: Update or generate a nexthook and save it to the Workin Progress Shook.

function updateWorkInProgressHook() :Hook {
  // This function is used both for updates and for re-renders triggered by a
  // render phase update. It assumes there is either a current hook we can
  // clone, or a work-in-progress hook from a previous render pass that we can
  // use as a base. When we reach the end of the base list, we must switch to
  // the dispatcher used for mounts.
  if(nextWorkInProgressHook ! = =null) {
    // There's already a work-in-progress. Reuse it.
    // Update next hook
    // If there is already a hook in the update phase, use that hook to get the next hook
    workInProgressHook = nextWorkInProgressHook;
    nextWorkInProgressHook = workInProgressHook.next;
    / / the current hookcurrentHook = nextCurrentHook; nextCurrentHook = currentHook ! = =null ? currentHook.next : null;
  } else {
    // Clone from the current hook.invariant( nextCurrentHook ! = =null.'Rendered more hooks than during the previous render.',);// Generate a new hook based on currenthook
    currentHook = nextCurrentHook;

    const newHook: Hook = {
      memoizedState: currentHook.memoizedState,

      baseState: currentHook.baseState,
      queue: currentHook.queue,
      baseUpdate: currentHook.baseUpdate,

      next: null};// Create or insert newHook into a circular list
    if (workInProgressHook === null) {
      // This is the first hook in the list.
      workInProgressHook = firstWorkInProgressHook = newHook;
    } else {
      // Append to the end of the list.
      workInProgressHook = workInProgressHook.next = newHook;
    }
    nextCurrentHook = currentHook.next;
  }
  return workInProgressHook;
}
Copy the code

Return updateReducer:

  // Last time I saw this
  const hook = updateWorkInProgressHook();
  // Start from here
  // Get the update queue list
  const queue = hook.queue;
  // If the update queue list is empty, an error is reportedinvariant( queue ! = =null.'Should have a queue. This is likely a bug in React. Please file an issue.',);// Save the reducer, which saves the update state
  queue.lastRenderedReducer = reducer;
  console.log(numberOfReRenders)  // Rerender is generally not required
  if (numberOfReRenders > 0) {... }Copy the code

numberOfReRenders

The numberOfReRenders count the number of times they re-render, and their updates are in the method renderWithHooks, which determines if the state needs to be updated during the Render phase, Namely didScheduleRenderPhaseUpdate attribute, if this property is true, that happened in the render phase heavy rendering, if need to apply colours to a drawing, the accounting Numbers.

export function renderWithHooks(
  current: Fiber | null,
  workInProgress: Fiber,
  Component: any,
  props: any,
  refOrContext: any,
  nextRenderExpirationTime: ExpirationTime,
) :any {...//ʚ whether state needs to be updated during render phase, indicating that rerender occurred during render phase
  if (didScheduleRenderPhaseUpdate) {
    do {
      didScheduleRenderPhaseUpdate = false;
      //ʚ Count if you need to re-render
      numberOfReRenders += 1; . }... }Copy the code

NumberOfReRenders < RE_RENDER_LIMIT (25 times) : Too many re-renders. The scenario is as follows:

function App() {
  const [a, seta] = useState(0)
  useEffect(() = > {
    console.log("useEffect")})return (
    <div className="App" onClick={seta(2)}>{a}</div>
  );
}
Copy the code

Output numberOfReRenders as shown

Return the updateReducer code:

If you need to re-render

  // The code you just saw
  if (numberOfReRenders > 0) {
    // Start from here
    // This is a re-render. Apply the new render phase updates to the previous
    // work-in-progress hook.
    // Get the dispatch in the update queue list
    const dispatch: Dispatch<A> = (queue.dispatch: any);
    RenderPhaseUpdates is the map we saw in the dispatchAction phase
    if(renderPhaseUpdates ! = =null) {
Copy the code

If you have renderPhaseUpdates

Action: Takes the saved updates from the map and iterates through them.

    if(renderPhaseUpdates ! = =null) {
      // Render phase updates are stored in a map of queue -> linked list
      // Get the update object in the queue
      const firstRenderPhaseUpdate = renderPhaseUpdates.get(queue);
      if(firstRenderPhaseUpdate ! = =undefined) {
        renderPhaseUpdates.delete(queue);
        // Get the current state
        let newState = hook.memoizedState;
        The update / / assignment
        let update = firstRenderPhaseUpdate;
        do {
          // Loop through and execute, using newState to catch the updated state
          const action = update.action;
          newState = reducer(newState, action);
          update = update.next;
        } while(update ! = =null);

        // Mark that the fiber performed work, but only if the new state is
        // different from the current state.
        // If the state is not the same as the last state, mark the node update
        if(! is(newState, hook.memoizedState)) {// The assignment operation, the source code will be given later
          markWorkInProgressReceivedUpdate();
        }
        // Update the current state
        hook.memoizedState = newState;
        // Don't persist the state accumulated from the render phase updates to
        // the base state unless the queue is empty.
        // queue.last, the last Update, which contains the latest state mounted to the queue at dispatch time
        // baseState, the state value since the last update, which is the argument passed to the function when it is executed
        // baseUpdate, the Update structure generated in the last rerender, is used to find the first Update node to be processed in this rerender (baseupdate.next).
        // Unless the update queue in the loop above is empty, there is no need to update the state accumulated in the render phase to basestate, i.e. wait until the update is complete before assigning ~
        if (hook.baseUpdate === queue.last) {
          hook.baseState = newState;
        }
        // Record the render state for this time
        queue.lastRenderedState = newState;

        return[newState, dispatch]; }}return [hook.memoizedState, dispatch];
  }
Copy the code
Hook. BaseState = newState

Test: code to simulate a rerender

function App() { const [a, Seta = useState(0) return (<div className="App" onClick={() => {console.log(" console.log ") seta(function (ii) { The console. The log (' 2 ', ii) return + + I})}} > {I} {I = = = 1 && seta (+ + I)} {I = = = 2 && seta (+ + I)} < / div > / / heavy rendering twice); }Copy the code
  1. Leave hook. BaseState = newState

  2. Comment hook. BaseState = newState

You can see that there is a problem with argument II passed to the callback function.

If no rerender occurs

Continuing with the code, here is what happens when no rerender occurs:

Queue. last: removes the update stored in queue.last and expands it into a regular linked list to iterate over the updates.

/ /... Return [hook. MemoizedState, dispatch]; } // The last update in The entire queue is null const last = queue.last; // The last update that is part of The base state. // The last update object const baseUpdate = hook.baseupdate; BaseState = hook. BaseState; baseState = hook. // console.log('baseState', baseState) // Find the first unprocessed update. let first; // console.log(baseUpdate ! If (baseUpdate!) ʚ null if (baseUpdate! == null) {// Update if (last! == null) { // For the first update, the queue is a circular linked list where // `queue.last.next = queue.first`. Once the first update commits, And // the 'baseUpdate' is no longer empty, we can unravel the list. } first = baseUpdate.next; // baseUpdate} else {// This is the first update, this is the loop list, to get the first update, use last.next. first = last ! == null ? last.next : null; }Copy the code

For first, there is the following code:

function App() {
  const [a, seta] = useState(0)
  return (
    <div className="App" onClick={()= > {
      seta(1)
      seta(2)
    }}>{i}</div>
  );
}
Copy the code
  1. The first click, the linked list is 1-2-1-2-1-2..
  2. On the second click, the list is 1-2-NULL

Why make a circular list and then expand it

The last update is the latest update. The circular linked list is made to make it easier to obtain the end node in the updateReducer phase. The unchained list is made because the updateReducer needs to be updated one by one. The update is not complete until next is null.

About Priority

Back to updateReducer further down, you can see the determination of the priority:

    // The code you just sawfirst = last ! = =null ? last.next : null;
  }
  // Start from here
  if(first ! = =null) {
    let newState = baseState;
    let newBaseState = null;
    let newBaseUpdate = null;
    let prevUpdate = baseUpdate;
    let update = first;
    let didSkip = false;
    do {
      const updateExpirationTime = update.expirationTime;
      if (updateExpirationTime < renderExpirationTime) {
Copy the code

ExpirationTime

To prevent an update from being continuously interrupted due to priority. React sets a ExpirationTime. When a ExpirationTime expires, React will force an update if it hasn’t been executed yet.

The code is not my word interpretation, mostly from blog looks, mainly refer to the article: zhuanlan.zhihu.com/p/108274604…

Start with the updateContainer method:

export function updateContainer(element: ReactNodeList, container: OpaqueRoot, parentComponent: ? React$Component<any, any>, callback: ?Function.) :ExpirationTime {
  const current = container.current;
  const currentTime = requestCurrentTime();
Copy the code
updateContainer-requestCurrentTime

Function: Gets currentEventTime, returns the current time or does no update depending on the case.

export function requestCurrentTime() {
  // The initial value of executionContext is NoContext, which is 0b000000
  // Execute Context is in the RenderContext or CommitContext stage
  if((executionContext & (RenderContext | CommitContext)) ! == NoContext) {// We're inside React, so it's fine to read the actual time.
    // This stage directly gets the current time
    return msToExpirationTime(now());
  }
  // We're not inside React, so we may be in the middle of a browser event.
  // If you are not in noWork, use the start time and wait until you work again.
  if(currentEventTime ! == NoWork) {// Use the same start time for all updates until we enter React again.
    return currentEventTime;
  }
  // This is the first update since React yielded. Compute a new start time.
  // The first update initializes a current time
  currentEventTime = msToExpirationTime(now());
  return currentEventTime;
}
Copy the code
msToExpirationTime

Function: Round up milliseconds in the unit of 10ms.

/ / 1073741823
export const Sync = MAX_SIGNED_31_BIT_INT;
/ / 1073741822
export const Batched = Sync - 1;

const UNIT_SIZE = 10;
/ / 1073741821
const MAGIC_NUMBER_OFFSET = Batched - 1;

// 1 unit of expiration time represents 10ms.
// 10 milliseconds is 1 unit, which facilitates batch updates
export function msToExpirationTime(ms: number) :ExpirationTime {
  // Always add an offset so that we don't clash with the magic number for NoWork.
  // 1073741821-((ms/10) | 0)
  / / | 0 means integer down, plus 1, rounded up
  return MAGIC_NUMBER_OFFSET - ((ms / UNIT_SIZE) | 0);
}
Copy the code

Go back to updateContainer and continue looking at the code

  const currentTime = requestCurrentTime();
  const expirationTime = computeExpirationForFiber(
    currentTime,
    current,
    suspenseConfig,
  );
Copy the code
updateContainer-computeExpirationForFiber

Function: Calculate the expiration time

export function computeExpirationForFiber(
  currentTime: ExpirationTime,
  fiber: Fiber,
  suspenseConfig: null | SuspenseConfig,
) :ExpirationTime {
  const mode = fiber.mode;
  // Whether it is NoMode, if so, it is synchronous rendering mechanism
  if ((mode & BatchedMode) === NoMode) {
    return Sync;
  }
  // Get the priority, source code is given below
  const priorityLevel = getCurrentPriorityLevel();
  // Check if it is BatchedMode
  if ((mode & ConcurrentMode) === NoMode) {
    return priorityLevel === ImmediatePriority ? Sync : Batched;
  }
  // Handle Concurrent Mode, which returns the previous renderExpirationTime
  if((executionContext & RenderContext) ! == NoContext) {// Use whatever time we're already rendering
    // TODO: Should there be a way to opt out, like with `runWithPriority`?
    return renderExpirationTime;
  }

  let expirationTime;
  if(suspenseConfig ! = =null) {
    // Compute an expiration time based on the Suspense timeout.
    // Calculate Suspense expiration time
    expirationTime = computeSuspenseExpiration(
      currentTime,
      suspenseConfig.timeoutMs | 0 || LOW_PRIORITY_EXPIRATION,
    );
  } else {
    // Compute an expiration time based on the Scheduler priority.
    // Calculate the expiration time according to the priority
    switch (priorityLevel) {
      case ImmediatePriority:
        MAX_SIGNED_31_BIT_INT
        expirationTime = Sync;
        break;
      case UserBlockingPriority:
        // TODO: Rename this to computeUserBlockingExpiration
        // Calculate the expiration time of the interaction event, the source code is given below
        expirationTime = computeInteractiveExpiration(currentTime);
        break;
      case NormalPriority:
      case LowPriority: // TODO: Handle LowPriority
        // TODO: Rename this to... something better.
        // Calculate the expiration time of asynchronous updates, source code is given below
        expirationTime = computeAsyncExpiration(currentTime);
        break;
      case IdlePriority:
        / / 1
        expirationTime = Idle;
        break;
      default:
        invariant(false.'Expected a valid priority level'); }}// If we're in the middle of rendering a tree, do not update at the same
  // expiration time that is already rendering.
  // TODO: We shouldn't have to do this if the update is on a different root.
  // Refactor computeExpirationForFiber + scheduleUpdate so we have access to
  // the root when we check for this condition.
  // If the tree is being rendered, do not update it at the same time as the tree that is already being rendered
  // Therefore reduce the priority
  if(workInProgressRoot ! = =null && expirationTime === renderExpirationTime) {
    // This is a trick to move this update into a separate batch
    expirationTime -= 1;
  }

  return expirationTime;
}
Copy the code
getCurrentPriorityLevel

Effect: Gets the priority

export function getCurrentPriorityLevel() :ReactPriorityLevel {
  // Scheduler_getCurrentPriorityLevel returns currentPriorityLevel;
  // D:\react\ReactSourceCodeAnalyze\source-code-demo\src\react\packages\scheduler\src\Scheduler.js
  CurrentPriorityLevel starts with NormalPriority, but may be assigned to the priorityLevel of the node. The previous priorityLevel will be used.
  switch (Scheduler_getCurrentPriorityLevel()) {
    //export const unstable_ImmediatePriority = 1;
    case Scheduler_ImmediatePriority:
      return ImmediatePriority;
    //export const unstable_UserBlockingPriority = 2;
    case Scheduler_UserBlockingPriority:
      return UserBlockingPriority;
    //export const unstable_NormalPriority = 3;
    case Scheduler_NormalPriority:
      return NormalPriority;
    //export const unstable_LowPriority = 4;
    case Scheduler_LowPriority:
      return LowPriority;
    //export const unstable_IdlePriority = 5;
    case Scheduler_IdlePriority:
      return IdlePriority;
    default:
      invariant(false.'Unknown priority level.'); }}Copy the code
ComputeInteractiveExpiration and computeAsyncExpiration

Function: Calculates the expiration time of synchronous and asynchronous

// We intentionally set a higher expiration time for interactive updates in
// dev than in production.
// We intentionally set a longer expiration time for interactive updates in the development environment than in the production environment
// If the main thread is being blocked so long that you hit the expiration,
// it's a problem that could be solved with better scheduling.
// If the main thread is blocked for too long causing the update to expire, this problem can be solved with better scheduling
// People will be more likely to notice this and fix it with the long
// expiration time in development.
// People are more likely to notice this and fix it with a longer expiration time during development.
// In production we opt for better UX at the risk of masking scheduling
// problems, by expiring fast.
// In a production environment, we chose a better user experience and hid the risk of scheduling problems by fast expiration.
export const HIGH_PRIORITY_EXPIRATION = __DEV__ ? 500 : 150;
export const HIGH_PRIORITY_BATCH_SIZE = 100;

export function computeInteractiveExpiration(currentTime: ExpirationTime) {
  return computeExpirationBucket(
    currentTime,
    HIGH_PRIORITY_EXPIRATION,   / / 500
    HIGH_PRIORITY_BATCH_SIZE,   / / 100
  );
}

// TODO: This corresponds to Scheduler's NormalPriority, not LowPriority. Update
// the names to reflect.
export const LOW_PRIORITY_EXPIRATION = 5000;
export const LOW_PRIORITY_BATCH_SIZE = 250;

export function computeAsyncExpiration(
  currentTime: ExpirationTime,
) :ExpirationTime {
  return computeExpirationBucket(
    currentTime,
    LOW_PRIORITY_EXPIRATION,   / / 5000
    LOW_PRIORITY_BATCH_SIZE,   / / 250
  );
}
Copy the code
computeExpirationBucket
/ / (num/precision) | 0) is rounded down, + 1 is rounded up
function ceiling(num: number, precision: number) :number {
  return (((num / precision) | 0) + 1) * precision;
}

function computeExpirationBucket(currentTime, expirationInMs, bucketSizeMs,) :ExpirationTime {
  // if the priority is low: 1073741821-ceiling(1073741821-currenttime +500,25)
  // 1073741821-((((1073741821-currentTime+500) / 25) | 0) + 1) * 25
  // Make sure the decimal point is multiplied by 25
  // if the priority is high: 1073741821-ceiling(1073741821-currenttime +50,10)
  // It must be a multiple of 10
  return (
    MAGIC_NUMBER_OFFSET -
    ceiling(
      MAGIC_NUMBER_OFFSET - currentTime + expirationInMs / UNIT_SIZE,
      bucketSizeMs / UNIT_SIZE,
    )
  );
}
Copy the code

The code behind updateContainer probably generates updates and schedules based on the generated expiration date. Look at that next time when WE look at Fiber.

Continue to see that no rerender has occurred

Effect: If there is a case of low priority, it is directly assigned to the result of the previous cycle, because it does not need to be executed yet, when it is executed, ensure that it is executed on this basis. If the update is of sufficient priority, it is recycled for update. When all updates have sufficient priority, assign the latest UPDATE and the latest state.

If you end up with a different state, mark the update.

Finally, update the hook.

    // The code you just saw
    do {
      const updateExpirationTime = update.expirationTime;
      // Start from here
      if (updateExpirationTime < renderExpirationTime) {
        // Priority is insufficient. Skip this update. If this is the first
        // skipped update, the previous update/state is the new base
        // update/state.
        // If the priority is small,
        if(! didSkip) { didSkip =true;
          // Set update and newstate directly, skipping subsequent assignments
          PrevUpdate and newState from the previous "round" loop
          // If the first loop goes here, you get the previous prevUpdate and newState
          // Because when it is executed, it is necessary to ensure that subsequent updates are executed on the basis of its updates, because the results may be different.
          newBaseUpdate = prevUpdate;
          newBaseState = newState;
        }
        // Update the remaining priority in the queue.
        if(updateExpirationTime > remainingExpirationTime) { remainingExpirationTime = updateExpirationTime; markUnprocessedUpdateTime(remainingExpirationTime); }}else {
        // This update does have sufficient priority.
        // This update has sufficient priority
        // Mark the event time of this update as relevant to this render pass.
        / / update the latest processing expiration time of the event, will update an attribute called workInProgressRootLatestProcessedExpirationTime, seemed to have judge handling the new update (if there are new updates in the rendering process, There may be new load states, submit them as soon as possible), and infer the timing of suspense updates.
        // TODO: This should ideally use the true event time of this update rather than
        // its priority which is a derived and not reverseable value.
        // TODO: We should skip this update if it was already committed but currently
        // we have no way of detecting the difference between a committed and suspended
        // update here.
        markRenderEventTimeAndConfig(
          updateExpirationTime,
          update.suspenseConfig,
        );

        // Process this update.
        // Handle this update
        if (update.eagerReducer === reducer) {
          // If this update was processed eagerly, and its reducer matches the
          // current reducer, we can use the eagerly computed state.
          If this update is real-time and the Reducer matches the current reducer, use the real-time state directly.
          // eagerState, which is useful if reducer has not changed at the time of entering the render state, it can directly use eagerState without further calculation.
          newState = ((update.eagerState: any): S);
        } else {
          // Otherwise, the state is recalculated
          constaction = update.action; newState = reducer(newState, action); }}// Update update is the next update
      prevUpdate = update;
      update = update.next;
    } while(update ! = =null&& update ! == first);// If all updates are of sufficient priority, the latest update and newState are assigned
    if(! didSkip) { newBaseUpdate = prevUpdate; newBaseState = newState; }// Mark that the fiber performed work, but only if the new state is
    // different from the current state.
    // The update is complete only if the status is different from the previous state
    if(! is(newState, hook.memoizedState)) { markWorkInProgressReceivedUpdate(); }// Update to the new status and update
    hook.memoizedState = newState;
    hook.baseUpdate = newBaseUpdate;
    hook.baseState = newBaseState;
    queue.lastRenderedState = newState;
  }
  
  // Return state and useState
  const dispatch: Dispatch<A> = (queue.dispatch: any);
  return [hook.memoizedState, dispatch];
Copy the code

The difference between memoizedState and baseState

MemoizedState is assigned: InitialState in mountState, newState in updateReducer.

Hook. BaseState is assigned to: Initialstate in mountState, newBaseState in updateReducer, and newBaseState will be assigned to the previous newState instead of the latest newState if the priority is lower. BaseUpdate === queue. Last is directly assigned newState.

Hook. BaseUpdate is the same as hook.

In other words, there are two cases in each cycle:

  1. If the priority is low

    NewBaseState should not be updated. NewBaseState will be assigned to the newState obtained when updated. Because later, when the priority is enough, you need to update on the basis of baseState.

  2. If the priority is high

    Should be updated, newState should be assigned normally. When the update is complete, assign the latest newState to newBaseState.

Other functions source

Invariant: Throws errors

export default function invariant(condition, format, a, b, c, d, e, f) {
  validateFormat(format);
 
  if(! condition) {let error;
    if (format === undefined) {
      error = new Error(
        'Minified exception occurred; use the non-minified dev environment ' +
          'for the full error message and additional helpful warnings.',); }else {
      const args = [a, b, c, d, e, f];
      let argIndex = 0;
      error = new Error(
        format.replace(/%s/g.function() {
          returnargs[argIndex++]; })); error.name ='Invariant Violation';
    }
 
    error.framesToPop = 1; // we don't care about invariant's own frame
    throwerror; }}Copy the code

Mark tag related functions

export function markWorkInProgressReceivedUpdate() {
  // Mark that the node update is complete
  didReceiveUpdate = true;
}
Copy the code

Some of the usual problems

Use prop as a parameter, and then change the prop asynchronously

function Child(prop){
  const [b, setb] = useState(`c${prop.b}`)
  return <p>{b}</p>
}

function App() {
  const [a, seta] = useState(0)
  useEffect(() = > {
    setTimeout(() = >{
      seta(10)},1000)})return <Child b={a}/>
}
Copy the code

Hey, guess what, you end up with a 0 on the page. Run through the entire state process to see:

When you first walk mountState:

  • The parent component

    1. Initialize a hook from the mountWorkInProgressHook and attach it to the hook queue

    2. Initialstate is known to be 0 by the parameter passed in, and is returned by attaching to the memoizedState property of the hook

    3. Initialize a queue

        const queue = (hook.queue = {
          last: null.dispatch: null.lastRenderedReducer: basicStateReducer,  // Calculate, if acrion is a value return action, if function return action(state)
          lastRenderedState: (initialState: any),  // Initial value, 0
        });
      Copy the code
    4. Return [memoizedState, dispatchAction(diber, queue)], pass in the action value for dispatchAction when setxxx(XXX) is called.

Run to return
to start mountState for the Child component.

  • Child components

    The workflow process is the same as the parent component, differing only from initialState, which is C0.

    baseState: "c0"
    baseUpdate: null
    memoizedState: "c0"
    next: null
    queueDispatch: ƒ ()last: null
        lastRenderedReducer: ƒ basicStateReducer (state, action)lastRenderedState: "c0"
    Copy the code

Run to

{b}

, render is complete, and c0 is displayed on the page.

Next, run useEffect setTimeout and run seta(10). So just run the dispatchAction function. This function initializes an update object, stores properties such as action, expiration time, priority, etc., and then uses update.next to generate a circular list, which is stored in queue.last.

const update: Update<S, A> = {
  expirationTime,
  suspenseConfig,
  action,
  eagerReducer: null.eagerState: null.next: null};Copy the code

Then save the lastRenderedReducer at that time in update.eagerReducer, and save the state (10) calculated by lastRenderedReducer at that time in update.eagerState. Comparing the calculated state with the state (0) of the last rendering, and finding a difference, scheduling begins to re-render the component.

After entering the updateReducer:

  • The parent component

    1. Const [a, seta] = useState(0)

    2. Perform updateWorkInProgressHook, can see the currentHook is null, and nextCurrentHook. Queue. The action = 10 (dispatchAction).

    3. Since there is no nextWorkInProgressHook, assign the nextCurrentHook to currentHook and copy the contents of currentHook to newHook.

      const newHook: Hook = {
        memoizedState: currentHook.memoizedState,
      
        baseState: currentHook.baseState,
        queue: currentHook.queue,
        baseUpdate: currentHook.baseUpdate,
      
        next: null};Copy the code

      After assigning the value to workInProgressHook, the linked list structure is formed, and the hook is finally thrown.

    4. The updateReducer function obtains the queue stored in the updateReducer function. The queue code is as follows:

      baseState: 0
      baseUpdate: null
      memoizedState: 0
      next: null
      queueDispatch: ƒ ()last:
              action: 10
              eagerReducer: ƒ basicStateReducer (state, action)eagerState: 10
              expirationTime: 1073741823
              // Loop linked list structure
              next: {expirationTime: 1073741823.suspenseConfig: null.action: 10.eagerState: 10.eagerReducer: ƒ,... }priority: 97
              suspenseConfig: null
      lastRenderedReducer: ƒ basicStateReducer (state, action)lastRenderedState: 0
      Copy the code
    5. Since no re-rendering has occurred, go directly to the unre-rendered parts. Use last.next to find the header (the first update) and unpack the update into a singly linked list.

    6. Last. Next =first=update. Next = reducer(newState, action);

    7. The resulting newState (10) is different from the last recorded hook.MemoizedState (0) in Hook, so it needs to be rerendered. Mark markWorkInProgressReceivedUpdate ().

    8. Finally, update hook state and update queue lastRenderedState.

    Next, useEffect, which is not seta(10) yet.

    1. The current currentHook is:

      Copy the code

    baseState: 0 baseUpdate: null memoizedState: 0 next: baseState: null baseUpdate: null memoizedState: {tag: 192, destroy: undefined, deps: null, next: {… }, create: ƒ} next: null queue: null queue: ƒ () last: {expirationTime: 1073741823, suspenseConfig: Null, action: 10, eagerState: 10, eagerReducer: ƒ,… ƒ basicStateReducer(State, Action) lastRenderedState: 10

    CurrentHook is: js baseState: NULL baseUpdate: NULL memoizedState: Create: () => {... } deps: null destroy: undefined next: {tag: 192, deps: null, next: {... }, create: ƒ} tag: 192 __proto__: Object next: null queue: null __proto__: ObjectCopy the code

    The value is assigned to the workInProgressHook and placed at the end of the list, and returned to the updateEffect function. The previous workInProgressHook looks like this.

    baseState: 10
    baseUpdate:
       action: 10
       eagerReducer: ƒ basicStateReducer (state, action)eagerState: 10
       expirationTime: 1073741823
       next: {expirationTime: 1073741823.suspenseConfig: null.action: 10.eagerState: 10.eagerReducer: ƒ,... }priority: 97
       suspenseConfig: null
       __proto__: Object
       memoizedState: 10
       next: null
       queueDispatch: ƒ ()last: {expirationTime: 1073741823.suspenseConfig: null.action: 10.eagerState: 10.eagerReducer: ƒ,... }lastRenderedReducer: ƒ basicStateReducer (state, action)lastRenderedState: 10
       __proto__: Object
       __proto__: Object
    Copy the code

    Then run to
    , go to renderWithHooks, then go to the updateReducer of the Child component to see if it needs to be rerendered.

    1. CurrentHook =nextCurrentHook; currentHook=nextCurrentHook;

      baseState: "c0"  // The last render value of the child component, which is the hook generated in the mount phase
      baseUpdate: null
      memoizedState: "c0"
      next: null
      queueDispatch: ƒ ()last: null
          lastRenderedReducer: ƒ basicStateReducer (state, action)lastRenderedState: "c0"
          __proto__: Object
      __proto__: Object
      Copy the code

      Copy it to newHook and put it at the end of the linked list.

    2. In this case, last=null, baseUpdate =null, so first can only be null.

      first = last ! == null ? last.next : null;

      MemoizedState [hook. MemoizedState, dispatch] return[‘c0’, dispatch]

    The immediate cause of the asynchrony problem is that the Update Workin Progresshook was not updated and next was still null when it was rendered again, causing the update phase to be skipped.

    The second time we run the component code, we will not use the initial value passed in useState to get the action. Instead, we will look for new updates in the hook, so we need to reset XXX. However, direct setxxx will cause an infinite loop, so you can useEffect[] to update prop only once after it changes.

  • The parent component

    Seta (10), useState(0), useState(0), useState(0), useState(0)

    baseState: 10
    baseUpdate:
    action: 10
    eagerReducer: ƒ basicStateReducer (state, action)eagerState: 10
    expirationTime: 1073741823
    next: {expirationTime: 1073741823.suspenseConfig: null.action: 10.eagerReducer: null.eagerState: null,... }priority: 97
    suspenseConfig: null
    __proto__: Object
    memoizedState: 10
    next: null
    queueDispatch: ƒ ()last: {expirationTime: 1073741823.suspenseConfig: null.action: 10.eagerReducer: null.eagerState: null,... }lastRenderedReducer: ƒ basicStateReducer (state, action)lastRenderedState: 10
    __proto__: Object
    __proto__: Object
    Copy the code

    The state value is found to be the same as before, so no update is made.

The react priority: xiaoxiaosaohuo. Making. IO/react – books…

Segmentfault.com/a/119000002…

Segmentfault.com/a/119000002…

www.zyiz.net/tech/detail…

www.cnblogs.com/zhongmeizhi…

www.sohu.com/a/402114001…