react source code analysis 13.hooks source code

Keywords: React

react source code analysis 13.hooks source code

Video Explanation (efficient learning): Enter learning

Previous articles:

1. Introduction and interview questions

2. Design concept of react

3.react source code architecture

4. Source directory structure and debugging

5. JSX & Core api

6.legacy and concurrent mode entry functions

7.Fiber architecture

8.render stage

9.diff algorithm

10.commit phase

11. Life cycle

12. Status update process

13.hooks source code

14. Handwritten hooks

15.scheduler&Lane

16.concurrent mode

17.context

18 event system

19. Handwritten Mini react

20. Summary & answers to interview questions in Chapter 1

hook call entry

In the hook source code, hook exists in Dispatcher, which is an object. The functions called by different hooks are different. The global variable ReactCurrentDispatcher.current will be assigned to HooksDispatcherOnMount or HooksDispatcherOnUpdate according to mount or update

ReactCurrentDispatcher.current = 
  current === null || current.memoizedState === null//mount or update
  ? HooksDispatcherOnMount
    : HooksDispatcherOnUpdate;  
const HooksDispatcherOnMount: Dispatcher = {//mount time
  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,
  //...
};

hook data structure

In the FunctionComponent, multiple hooks will form a hook linked list, which is saved in the memoizedState of Fiber, and the Update to be updated is saved in hook.queue.pending

const hook: Hook = {
  memoizedState: null,//There are different values for different hook s
  baseState: null,//Initial state
  baseQueue: null,//Initial queue
  queue: null,//Update to update
  next: null,//Next hook
};

Let's look at the values corresponding to memoizedState

  • useState: for example, const [state, updateState] = useState(initialState), memoizedState equals the value of state
  • useReducer: for example, const [state, dispatch] = useReducer(reducer, {});, memoizedState is equal to the value of state
  • useEffect: during mountEffect, pushEffect will be called to create the effect linked list. memoizedState is equal to the effect linked list. The effect linked list will also be attached to fiber.updateQueue. The first parameter callback and the second parameter dependency array of useEffect exist on each effect. For example, useEffect(callback, [dep]), and the effect is {create:callback, dep:dep,...}
  • useRef: for example, useRef(0), memoizedState is equal to {current: 0}
  • useMemo: for example, useMemo(callback, [dep]), memoizedState equals [callback(), dep]
  • useCallback: for example, useCallback(callback, [dep]), memoizedState equals [callback, dep]. useCallback saves the callback function, and useMemo saves the execution result of the callback

useState&useReducer

The reason why useState and useReducer are put together is that in the source code, useState is a useReducer with default reducer parameters.

  • Usestate & usereducer declaration

    The resolveDispatcher function gets 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);
    }
    
  • mount phase

    In the mount stage, useState calls mountState and useReducer calls mountReducer. The only difference is that the lastRenderedReducer in the queue they create is different. Mount has an initial value of basicStateReducer. 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 = {//Assign hook.queue
        pending: null,
        dispatch: null,
        lastRenderedReducer: basicStateReducer,//Difference between and mountReducer
        lastRenderedState: (initialState: any),
      });
      const dispatch: Dispatch<//Create dispatch function
        BasicStateAction<S>,
      > = (queue.dispatch = (dispatchAction.bind(
        null,
        currentlyRenderingFiber,
        queue,
      ): 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 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
    }
    
function basicStateReducer<S>(state: S, action: BasicStateAction<S>): S {
  return typeof action === 'function' ? action(state) : action;
}
  • update phase

    When updating, the new state will be calculated according to the update in the hook

    function updateReducer<S, I, A>(
      reducer: (S, A) => S,
      initialArg: I,
      init?: I => S,
    ): [S, Dispatch<A>] {
      const hook = updateWorkInProgressHook();//Get hook
      const queue = hook.queue;
      queue.lastRenderedReducer = reducer;
    
      //... the updated state is basically consistent with the state calculation logic in Chapter 12
    
      const dispatch: Dispatch<A> = (queue.dispatch: any);
      return [hook.memoizedState, dispatch];
    }
    
  • Execution phase

    useState will call dispatchAction after executing setState. What dispatchAction does is to add Update to queue.pending, and then start scheduling

    function dispatchAction(fiber, queue, action) {
    
      var update = {//Create update
        eventTime: eventTime,
        lane: lane,
        suspenseConfig: suspenseConfig,
        action: action,
        eagerReducer: null,
        eagerState: null,
        next: null
      }; 
    
      //Add update to queue.pending
      
      var alternate = fiber.alternate;
    
      if (fiber === currentlyRenderingFiber$1 || alternate !== null && alternate === currentlyRenderingFiber$1) {
        //If it is an update executed in the render phase, didScheduleRenderPhaseUpdate=true
    }
        didScheduleRenderPhaseUpdateDuringThisPass = didScheduleRenderPhaseUpdate = true;
      } else {
        if (fiber.lanes === NoLanes && (alternate === null || alternate.lanes === NoLanes)) {
          //If the fiber does not have priority and the current alternate does not exist or does not have priority, there is no need to update
          //Optimization steps
        }
    
        scheduleUpdateOnFiber(fiber, lane, eventTime);
      }
    }
    

useEffect

  • 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);
}
  • mount phase

    Call mountEffect. mountEffect calls mountEffectImpl. hook.memoizedState is assigned to the effect linked list

function mountEffectImpl(fiberFlags, hookFlags, create, deps): void {
  const hook = mountWorkInProgressHook();//Get hook
  const nextDeps = deps === undefined ? null : deps;//rely on
  currentlyRenderingFiber.flags |= fiberFlags;//Add flag
  hook.memoizedState = pushEffect(//memoizedState=effects circular linked list
    HookHasEffect | hookFlags,
    create,
    undefined,
    nextDeps,
  );
}
  • update phase

    Shallow comparison dependency. If the dependency changes, the first parameter of pushEffect is passed to HookHasEffect | hookFlags. HookHasEffect indicates that the useEffect dependency has changed and needs to be re executed 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)) {//Compare deps
        //Even if the dependencies are equal, add the effect to the linked list to ensure consistent order
        pushEffect(hookFlags, create, destroy, nextDeps);
        return;
      }
    }
  }

  currentlyRenderingFiber.flags |= fiberFlags;

  hook.memoizedState = pushEffect(
    //The parameter is passed to HookHasEffect | hookFlags. The useEffect containing hookFlags will execute this effect in the commit phase
    HookHasEffect | hookFlags,
    create,
    destroy,
    nextDeps,
  );
}
  • Execution phase

    In the ninth chapter, the commitLayoutEffects function in the commit stage will call schedulePassiveEffects, destroy the useEffect destructor and callback function push to pendingPassiveHookEffectsUnmount and pendingPassiveHookEffectsMount, then invoke flushPassiveEffects to perform the last render destructor function callback and the callback function of this render after mutation.

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();//Destroy function execution
    } 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;//The creation function of render
   effect.destroy = create();
  } catch (error) {
    captureCommitPhaseError(fiber, error);
  }
}

useRef

sring type refs are no longer recommended (string s in the source code will generate refs, which occurs in the coerceRef function). ForwardRef just passes refs through parameters. createRef is also a {current: any structure, so we only discuss the useRef of function or {current: any}

//createRef returns {current: any}
export function createRef(): RefObject {
  const refObject = {
    current: null,
  };
  return refObject;
}
  • Declaration phase

    Like other hook s

export function useRef<T>(initialValue: T): {|current: T|} {
  const dispatcher = resolveDispatcher();
  return dispatcher.useRef(initialValue);
}
  • mount phase

    When mount, mountRef is called to create hook and ref objects.

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

render stage: add Ref Tag to the Fiber tag with ref attribute, which occurs in markRef in beginWork and completeWork functions

export const Ref = /*                          */ 0b0000000010000000;
//In 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;
  }
}
//In completeWork
function markRef(workInProgress: Fiber) {
  workInProgress.effectTag |= Ref;
}

commit phase:

Judge whether the ref is changed in the commitMutationEffects function. If it is changed, execute commitDetachRef first, delete the previous ref, and then execute committatchref assignment ref in 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 ref
      }
    }
  }
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}
    }
  }
}
function commitAttachRef(finishedWork: Fiber) {
  const ref = finishedWork.ref;
  if (ref !== null) {
    const instance = finishedWork.stateNode;//Gets 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;
    }
  }
}
  • update phase

    When updating, call update ref to get the current useRef, and then return to the hook linked list

function updateRef<T>(initialValue: T): {|current: T|} {
  const hook = updateWorkInProgressHook();//Get current useRef
  return hook.memoizedState;//Return to hook linked list
}

useMemo&useCallback

  • Declaration phase

    Like other hook s

  • mount phase

    The only difference between mount stage useMemo and useCallback is whether callback is stored in memoizedState or the function calculated by callback

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();//Calculate value
  hook.memoizedState = [nextValue, nextDeps];//Save value and dependency 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 dependency in memoizedState
  return callback;
}
  • update phase

    The same is true for update. The only difference is whether the callback function is used directly or the value returned after the callback is executed is assigned to memoizedState as [?, nextDeps]

function updateMemo<T>(
  nextCreate: () => T,
  deps: Array<mixed> | void | null,
): T {
  const hook = updateWorkInProgressHook();//Get hook
  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 comparison dependence
        return prevState[0];//No change, return to the previous state
      }
    }
  }
  const nextValue = nextCreate();//Call back again if there is a change
  hook.memoizedState = [nextValue, nextDeps];
  return nextValue;
}

function updateCallback<T>(callback: T, deps: Array<mixed> | void | null): T {
  const hook = updateWorkInProgressHook();//Get hook
  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 comparison dependence
        return prevState[0];//No change, return to the previous state
      }
    }
  }

  hook.memoizedState = [callback, nextDeps];//If changed, assign [callback, nextDeps] to memoizedState again
  return callback;
}

useLayoutEffect

useLayoutEffect is the same as useEffect, except that it is called at different times. It is executed synchronously in the commitLayout function in the commit phase

forwardRef

forwardRef is also very simple, that is, passing the ref attribute

export function forwardRef<Props, ElementType: React$ElementType>(
  render: (props: Props, ref: React$Ref<ElementType>) => React$Node,
) {
  
  const elementType = {
    $$typeof: REACT_FORWARD_REF_TYPE,
    render,
  };
  
  return elementType;
}
//The second parameter of ForwardRef is the ref object
let children = Component(props, secondArg);

Posted by raker7 on Fri, 03 Dec 2021 23:03:09 -0800