React source code

9. Hooks: Function Component saves state

Video lessons & Debug demos

The purpose of the video course is to quickly master the process of running the React source code and scheduler, Reconciler, Renderer, Fiber, etc., in react, and debug the source code and analysis in detail to make the process clearer.

Video course: Enter the course

Demos: demo

Course Structure:

  1. You’re still struggling with the React source code.
  2. The React Mental Model
  3. Fiber(I’m dom in memory)
  4. Start with Legacy or Concurrent (start at the entrance and let’s move on to the future)
  5. State update process (what’s going on in setState)
  6. Render phase (awesome, I have the skill to create Fiber)
  7. Commit phase (I heard renderer marked it for us, let’s map real nodes)
  8. Diff algorithm (Mom doesn’t worry about my Diff interviews anymore)
  9. Function Component saves state
  10. Scheduler&lane model (to see tasks are paused, continued, and queue-jumped)
  11. What is concurrent mode?
  12. Handwriting mini React (Short and Sharp is me)

Hook call entry

In hook source code, hook exists in the Dispatcher. Dispatcher is an object. Different hooks call different functions. Global variable ReactCurrentDispatcher. Current according to the mount or update assignment for HooksDispatcherOnMount or HooksDispatcherOnUpdate

ReactCurrentDispatcher.current = 
  current === null || current.memoizedState === null//mount or update
  ? HooksDispatcherOnMount
	: HooksDispatcherOnUpdate;  
Copy the code
const HooksDispatcherOnMount: Dispatcher = {/ / the mount
  useCallback: mountCallback,
  useContext: readContext,
  useEffect: mountEffect,
  useImperativeHandle: mountImperativeHandle,
  useLayoutEffect: mountLayoutEffect,
  useMemo: mountMemo,
  useReducer: mountReducer,
  useRef: mountRef,
  useState: mountState,
  / /...
};

const HooksDispatcherOnUpdate: Dispatcher = {/ / update
  useCallback: updateCallback,
  useContext: readContext,
  useEffect: updateEffect,
  useImperativeHandle: updateImperativeHandle,
  useLayoutEffect: updateLayoutEffect,
  useMemo: updateMemo,
  useReducer: updateReducer,
  useRef: updateRef,
  useState: updateState,
  / /...
};
Copy the code

Hook data structure

In FunctionComponent, multiple hooks form a list of hooks that are stored on Fiber’s memoizedState, while updates that need to be updated are stored in hook.queue.pending

const hook: Hook = {
  memoizedState: null.// There are different values for different hooks
  baseState: null./ / the initial state
  baseQueue: null.// Initialize queue
  queue: null.// Update that needs to be updated
  next: null.// Next hook
};
Copy the code

Now let’s look at the values of memoizedState

  • UseState: e.g.const [state, updateState] = useState(initialState).MemoizedState equalsThe value of the state
  • UseReducer: e.g.const [state, dispatch] = useReducer(reducer, {});.MemoizedState equalsThe value of the state
  • UseEffect: When mountEffect calls pushEffect to create an effect list,memoizedStateThis is equivalent to the effect list, which is also mounted to fiber.updateQueue. Each effect has the first callback and the second dependency array for useEffect, for example,useEffect(callback, [dep]), effect is {create:callback, dep:dep… }
  • UseRef: e.g.useRef(0), memoizedStateIs equal to{current: 0}
  • UseMemo: e.g.useMemo(callback, [dep]).memoizedStateIs equal to the[callback(), dep]
  • UseCallback: e.g.useCallback(callback, [dep]).memoizedStateIs equal to the[callback, dep].useCallbacksavecallbackThe function,useMemosavecallbackExecution result of

useState&useReducer

UseState and useReducer are put together because useState is the useReducer with the default reducer parameters in the source code.

  • UseState&useReducer statement

    The resolveDispatcher function retrieves the current Dispatcher

    function useState(initialState) {
      var dispatcher = resolveDispatcher();
      return dispatcher.useState(initialState);
    }
    function useReducer(reducer, initialArg, init) {
      var dispatcher = resolveDispatcher();
      return dispatcher.useReducer(reducer, initialArg, init);
    }
    
    Copy the code
  • Mount stage

    In the mount stage, useState calls mountState and useReducer calls mountReducer. The only difference is that the lastRenderedReducer in the queue created by useState and useReducer are different. Therefore, useState is a useReducer with default reducer parameters.

    function mountState<S> (//
      initialState: (() => S) | S,
    ) :S.Dispatch<BasicStateAction<S> >]{
      const hook = mountWorkInProgressHook();// Create current hook
      if (typeof initialState === 'function') {
        initialState = initialState();
      }
      hook.memoizedState = hook.baseState = initialState;/ / hook. MemoizedState assignment
      const queue = (hook.queue = {/ / assignment hook. The queue
        pending: null.dispatch: null.lastRenderedReducer: basicStateReducer,// mountReducer
        lastRenderedState: (initialState: any),
      });
      const dispatch: Dispatch<// Create dispatch function
        BasicStateAction<S>,
      > = (queue.dispatch = (dispatchAction.bind(
        null,
        currentlyRenderingFiber,
        quewque,
      ): any));
      return [hook.memoizedState, dispatch];// Return memoizedState and Dispatch
    }
    
    function mountReducer<S.I.A> (reducer: (S, A) => S, initialArg: I, init? : I => S,) :S.Dispatch<A>] {
      const hook = mountWorkInProgressHook();// Create current hook
      let initialState;
      if(init ! = =undefined) {
        initialState = init(initialArg);
      } else {
        initialState = ((initialArg: any): S);
      }
      hook.memoizedState = hook.baseState = initialState;/ / hook. MemoizedState assignment
      const queue = (hook.queue = {/ / create the queue
        pending: null.dispatch: null.lastRenderedReducer: reducer,
        lastRenderedState: (initialState: any),
      });
      const dispatch: Dispatch<A> = (queue.dispatch = (dispatchAction.bind(// Create dispatch function
        null,
        currentlyRenderingFiber,
        queue,
      ): any));
      return [hook.memoizedState, dispatch];// Return memoizedState and Dispatch
    }
    
    Copy the code
    function basicStateReducer<S>(state: S, action: BasicStateAction<S>): S {
      return typeof action === 'function' ? action(state) : action;
    }
    Copy the code
  • The update phase

    Update calculates the new state based on update in the hook

    function updateReducer<S, I, A>( reducer: (S, A) => S, initialArg: I, init? : I => S, ): [S, Dispatch<A>] { const hook = updateWorkInProgressHook(); Hook const queue = hook. Queue; queue.lastRenderedReducer = reducer; / /... Const dispatch: dispatch <A> = (queue.dispatch: any); return [hook.memoizedState, dispatch]; }Copy the code
  • Execution phase

    UseState executes setState and dispatchAction is called. All dispatchAction does is add Update to queue.pending and then dispatch

    Function dispatchAction(fiber, queue, action) {var update = { lane, suspenseConfig: suspenseConfig, action: action, eagerReducer: null, eagerState: null, next: null }; Update var alternate = fiber. Pending; if (fiber === currentlyRenderingFiber$1 || alternate ! = = null && alternate = = = currentlyRenderingFiber $1) {/ / if it's render phase perform update didScheduleRenderPhaseUpdate = true} didScheduleRenderPhaseUpdateDuringThisPass = didScheduleRenderPhaseUpdate = true; } else { if (fiber.lanes === NoLanes && (alternate === null || alternate.lanes === NoLanes)) { ScheduleUpdateOnFiber (Fiber, Lane, eventTime);} scheduleUpdateOnFiber(Fiber, Lane, eventTime); }}Copy the code

useEffect

  • The statement

    Gets and returns the useEffect function

    export function useEffect(
      create: () => (() => void) | void,
      deps: Array<mixed> | void | null.) :void {
      const dispatcher = resolveDispatcher();
      return dispatcher.useEffect(create, deps);
    }
    Copy the code
  • Mount stage

    Call mountEffect, mountEffect calls mountEffectImpl, hook. MemoizedState assigns an effect linked list

    function mountEffectImpl(fiberFlags, hookFlags, create, deps) :void {
      const hook = mountWorkInProgressHook();/ / get the hooks
      const nextDeps = deps === undefined ? null : deps;/ / rely on
      currentlyRenderingFiber.flags |= fiberFlags;/ / increase the flag
      hook.memoizedState = pushEffect(//memoizedState= Effects circular list
        HookHasEffect | hookFlags,
        create,
        undefined,
        nextDeps,
      );
    }
    Copy the code
  • The update phase

    Light is dependent on, if changed pushEffect first parameter dependent preach HookHasEffect | hookFlags, HookHasEffect said useEffect dependencies changed, need to perform in the commit phase

    function updateEffectImpl(fiberFlags, hookFlags, create, deps) :void {
      const hook = updateWorkInProgressHook();
      const nextDeps = deps === undefined ? null : deps;
      let destroy = undefined;
    
      if(currentHook ! = =null) {
        const prevEffect = currentHook.memoizedState;
        destroy = prevEffect.destroy;//
        if(nextDeps ! = =null) {
          const prevDeps = prevEffect.deps;
          if (areHookInputsEqual(nextDeps, prevDeps)) {//比较deps
            // Add effects to the list even if the dependencies are equal to ensure that the order is consistent
            pushEffect(hookFlags, create, destroy, nextDeps);
            return;
          }
        }
      }
    
      currentlyRenderingFiber.flags |= fiberFlags;
    
      hook.memoizedState = pushEffect(
        / / parameter transmission HookHasEffect | hookFlags containing hookFlags useEffect will perform this effect in the commit phase
        HookHasEffect | hookFlags,
        create,
        destroy,
        nextDeps,
      );
    }
    Copy the code
  • Execution phase

    SchedulePassiveEffects is called in the commitLayoutEffects function during the Commit phase of Chapter 9. UseEffect destruction and the callback function to push pendingPassiveHookEffectsUnmount and pendingPassiveHookEffectsMount, After mutation, flushPassiveEffects were called to execute the destruction function callback of last render and the callback function of this render successively

    const unmountEffects = pendingPassiveHookEffectsUnmount;
    pendingPassiveHookEffectsUnmount = [];
    for (let i = 0; i < unmountEffects.length; i += 2) {
      const effect = ((unmountEffects[i]: any): HookEffect);
      const fiber = ((unmountEffects[i + 1]: any): Fiber);
      const destroy = effect.destroy;
      effect.destroy = undefined;
    
      if (typeof destroy === 'function') {
        try {
          destroy();// The destruction function executes
        } catch(error) { captureCommitPhaseError(fiber, error); }}}const mountEffects = pendingPassiveHookEffectsMount;
    pendingPassiveHookEffectsMount = [];
    for (let i = 0; i < mountEffects.length; i += 2) {
      const effect = ((mountEffects[i]: any): HookEffect);
      const fiber = ((mountEffects[i + 1]: any): Fiber);
      
      try {
        const create = effect.create;// Create function of render
       effect.destroy = create();
      } catch(error) { captureCommitPhaseError(fiber, error); }}Copy the code

useRef

Sring refs are no longer recommended. The ForwardRef simply passes refs to the ForwardRef. CreateRef is {current: any} useRef

CreateRef returns {current: any}
export function createRef() :RefObject {
  const refObject = {
    current: null};return refObject;
}
// the ForwardRef second argument is the ref object
let children = Component(props, secondArg);
Copy the code
  • The statement phase

    Just like any other hook

    export function useRef<T> (initialValue: T) :{|current: T|} {
      const dispatcher = resolveDispatcher();
      return dispatcher.useRef(initialValue);
    }
    Copy the code
  • Mount stage

    When mount is called mountRef, the hook and ref objects are created.

    function mountRef<T> (initialValue: T) :{|current: T|} {
      const hook = mountWorkInProgressHook();/ / get useRef
      const ref = {current: initialValue};/ / ref initialization
      hook.memoizedState = ref;
      return ref;
    }
    Copy the code

    The Render stage: mark Fiber with a ref Tag, which occurs in the beginWork and completeWork functions in one step

    export const Ref = / * * / 0b0000000010000000;
    Copy the code
    / / beginWork
    function markRef(current: Fiber | null, workInProgress: Fiber) {
      const ref = workInProgress.ref;
      if (
        (current === null&& ref ! = =null) || (current ! = =null && current.ref !== ref)
      ) {
        workInProgress.effectTag |= Ref;
      }
    }
    / / completeWork
    function markRef(workInProgress: Fiber) {
      workInProgress.effectTag |= Ref;
    }
    Copy the code

    The commit phase:

    If the ref changes, it is checked in the commitMutationEffects function. If the ref changes, the commitDetachRef is first deleted, and then the commitAttachRef is assigned in the commitLayoutEffect.

    function commitMutationEffects(root: FiberRoot, renderPriorityLevel) {
      while(nextEffect ! = =null) {
        const effectTag = nextEffect.effectTag;
        // ...
        
        if (effectTag & Ref) {
          const current = nextEffect.alternate;
          if(current ! = =null) {
            commitDetachRef(current);/ / remove the ref}}}Copy the code
    function commitDetachRef(current: Fiber) {
      const currentRef = current.ref;
      if(currentRef ! = =null) {
        if (typeof currentRef === 'function') {
          currentRef(null);// if the type is function, call
        } else {
          currentRef.current = null;// Otherwise assign {current: null}}}}Copy the code
    function commitAttachRef(finishedWork: Fiber) {
      const ref = finishedWork.ref;
      if(ref ! = =null) {
        const instance = finishedWork.stateNode;// Get an instance of ref
        let instanceToUse;
        switch (finishedWork.tag) {
          case HostComponent:
            instanceToUse = getPublicInstance(instance);
            break;
          default:
            instanceToUse = instance;
        }
    
        if (typeof ref === 'function') {/ / ref assignment
          ref(instanceToUse);
        } else{ ref.current = instanceToUse; }}}Copy the code
  • The update phase

    Update call updateRef to retrieve the current useRef and return the hook list

    function updateRef<T> (initialValue: T) :{|current: T|} {
      const hook = updateWorkInProgressHook();// Get the current useRef
      return hook.memoizedState;// return the list of hooks
    }
    Copy the code

useMemo&useCallback

  • The statement phase

    Just like any other hook

  • Mount stage

    The only difference between useMemo and useCallback in the mount phase is whether callback is stored in memoizedState or the function that callback evaluates

    function mountMemo<T> (
      nextCreate: () => T,
      deps: Array<mixed> | void | null.) :T {
      const hook = mountWorkInProgressHook();/ / create a hook
      const nextDeps = deps === undefined ? null : deps;
      const nextValue = nextCreate();/ / the value calculation
      hook.memoizedState = [nextValue, nextDeps];// Save the value and dependencies in memoizedState
      return nextValue;
    }
    
    function mountCallback<T> (callback: T, deps: Array<mixed> | void | null) :T {
      const hook = mountWorkInProgressHook();/ / create a hook
      const nextDeps = deps === undefined ? null : deps;
      hook.memoizedState = [callback, nextDeps];// Save callback and dependencies in memoizedState
      return callback;
    }
    Copy the code
  • The update phase

    Update is the same, the only difference is whether the callback function is assigned to memoizedState directly or the value returned after the callback is executed as [?, nextDeps]

function updateMemo<T> (
  nextCreate: () => T,
  deps: Array<mixed> | void | null.) :T {
  const hook = updateWorkInProgressHook();/ / get the hooks
  const nextDeps = deps === undefined ? null : deps;
  const prevState = hook.memoizedState;

  if(prevState ! = =null) {
    if(nextDeps ! = =null) {
      const prevDeps: Array<mixed> | null = prevState[1];
      if (areHookInputsEqual(nextDeps, prevDeps)) {// Shallow dependencies
        return prevState[0];// Return to the same state}}}const nextValue = nextCreate();// Call callback again if changes occur
  hook.memoizedState = [nextValue, nextDeps];
  return nextValue;
}

function updateCallback<T> (callback: T, deps: Array<mixed> | void | null) :T {
  const hook = updateWorkInProgressHook();/ / get the hooks
  const nextDeps = deps === undefined ? null : deps;
  const prevState = hook.memoizedState;

  if(prevState ! = =null) {
    if(nextDeps ! = =null) {
      const prevDeps: Array<mixed> | null = prevState[1];
      if (areHookInputsEqual(nextDeps, prevDeps)) {// Shallow dependencies
        return prevState[0];// Return to the same state
      }
    }
  }

  hook.memoizedState = [callback, nextDeps];// Reassign [callback, nextDeps] to memoizedState
  return callback;
}

Copy the code