preface

It’s time to know why. Here’s an interview question (most React developers have encountered)

Interviewer: What is the difference between a function component and a class component?

A: Several things.

  • Components created using function are called “stateless components” and components created using class are called “stateful components”
  • A “stateless component” can only accept functions as input parameters (props). In addition to the read-only property this.props, the stateful component has a this.state property that holds private data, which can be read or written.
  • Function has no trouble with this. Because you can’t and don’t have to write this inside function. Roar! (break)
  • Stateful Components have a life cycle. Stateless components have no life cycle.

The essential difference between stateless components and stateless components is that the class state is maintained by an instantiated class. But in the Function component, there is no state mechanism to hold this information. Each time the function context is executed, all variables and constants are redeclared and then garbage collected.

This is a trap for the interviewer, you can either continue to ask for React hooks, function execution context, or V8 GC. This is called interview psychology, learn some ~ 2333333)

React hooks are not that hard to understand. In essence, they provide a state management mechanism for “stateless components” that add solutions to the problem. For example, logic cannot be reused, and pure function balance cannot be gracefully broken.

Come on, warm up

You can answer several of the following questions.

  1. Each time a stateless component executes in the function context,”React** Which method records "**Hooks**"State of **?
  2. ‘** How do I record the sequence in which each hook is used?
  3. Why not declare hooks in conditional statements?
  4. function** "useState" in component* * and”calssWhat is the difference in setState for a component?
  5. “UseEffect”**, "useMemo"** requires dependency injection, why not “useRef” ‘**?
  6. How does “useMemo” ** cache components?
  7. Why is the function component not updated when “useState” ‘** is called multiple times?
  8. Can you implement these hooks manually?

The React principle of Hooks

Everything from the use of analysis:

import { useState } from 'react'


function A () {

    const [xx,setXx] = useState(' ')}Copy the code

What happens when you import “useState”? Nonsense, of course, is to execute the source code……

Path:  React/cjs/react.development.js

Stickers are a little ugly, so I won’t paste them later…

// Line 1495

function useState(initialState) {

  var dispatcher = resolveDispatcher();

  return dispatcher.useState(initialState);

}
Copy the code

UseState is the result of resolveDispatcher. So, moving on to resolveDispatcher.


function resolveDispatcher() {

  var dispatcher = ReactCurrentDispatcher.current;


  if(! (dispatcher ! = =null)) {{throw Error( "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:\n1. You might have mismatching versions of React and the renderer (such as React DOM)\n2. You might be breaking the Rules of Hooks\n3. You might have more than one copy of React in the same app\nSee https://fb.me/react-invalid-hook-call for tips about how to debug and fix this problem."); }}return dispatcher;

}
Copy the code

Case 1: Why do hooks have to be used inside functions?

The source code is easy to understand, ReactCurrentDispatcher. Current is the current dispatcher



/** * Keeps track of the current dispatcher. */

var ReactCurrentDispatcher = {

  / * * *@internal

   * @type {ReactComponent}* /

  current: null

};
Copy the code

Read the comments first. Keep track of the current scheduler. Keep track of the current scheduler. Look again at the field comment for current: ReactComponent. **** means that the dispatcher must be the React component.

OK, source code to see this starts to be awkward. The initial value of current is null, and then 😅 is lost. Since the useState road has not been completed, it can only be seen from the upper level of useState.

How is function implemented?

Function uses React hooks internally. So we know that in the React architecture, what is called and when is coordinated by the coordination layer. So we can look at the React-Reconciler with suspicion

Path: react/packages/react-reconciler/src/ReactFiberBeginWork.new.js

You’ll see a lot of “renderWithHooks” calls in the source code. As you know from the name, this method is used to render the function component.


export function renderWithHooks<Props.SecondArg> (

  current: Fiber | null.// If current is initialized, current is null

  workInProgress: Fiber, // *workInProgress Fiber*

  Component: (p: Props, arg: SecondArg) => any, // function Component itself

  props: Props,

  secondArg: SecondArg,

  nextRenderLanes: Lanes, // Next render pass. It used to be called render expiration time

) :any {

  renderLanes = nextRenderLanes;

  currentlyRenderingFiber = workInProgress; // &1 *workInProgress* Let's label it and explain it later


  if(__DEV__) { hookTypesDev = current ! = =null

        ? ((current._debugHookTypes: any): Array<HookType>)

        : null;

    hookTypesUpdateIndexDev = -1;

    // Used for hot reloading:ignorePreviousDependencies = current ! = =null&& current.type ! == workInProgress.type; } workInProgress.memoizedState =null; // &2 cache state

  workInProgress.updateQueue = null; // &3 updates the queue

  workInProgress.lanes = NoLanes;

  if (__DEV__) {

   / / code...

  } else {

    ReactCurrentDispatcher.current =

      current === null || current.memoizedState === null

        ? HooksDispatcherOnMount

        : HooksDispatcherOnUpdate;

  }


  let children = Component(props, secondArg); // **Component(props, secondArg)**


  // Check if there was a render phase update

  if (didScheduleRenderPhaseUpdateDuringThisPass /* During this process, plan whether the render stage will be updated */) {

    // Keep rendering in a loop for as long as render phase updates continue to

    // be scheduled. Use a counter to prevent infinite loops.

    let numberOfReRenders: number = 0;

    do {

      didScheduleRenderPhaseUpdateDuringThisPass = false;

      invariant(

        numberOfReRenders < RE_RENDER_LIMIT,

        'Too many re-renders. React limits the number of renders to prevent ' +

          'an infinite loop.',); numberOfReRenders +=1;

      / / code...


      ReactCurrentDispatcher.current = __DEV__

        ? HooksDispatcherOnRerenderInDEV

        : HooksDispatcherOnRerender;

      children = Component(props, secondArg);

    } while (didScheduleRenderPhaseUpdateDuringThisPass);

  }



  // We can assume the previous dispatcher is always this one, since we set it

  // at the beginning of the render phase and there's no re-entrance.

  ReactCurrentDispatcher.current = ContextOnlyDispatcher;

    // code... Too much source code, part of the paste.

  constdidRenderTooFewHooks = currentHook ! = =null&& currentHook.next ! = =null;

    // code... Too much source code, part of the paste.invariant( ! didRenderTooFewHooks,'Rendered fewer hooks than expected. This may be caused by an accidental ' +

      'early return statement.',);return children;

}

Copy the code

Call ~! Here’s a summary:

  • RenderWithHooks are a higher-order function that eventually returns the function component itself
  • Start executing function component, initialize”WorkInProgress Fiber tree"MemoizedState"And”updateQueue“.why?
    • This is because the new hooks information (UPDATE) is mounted to these two properties, and then the workInProgress tree is replaced with the current tree at the component commit phase, replacing the actual DOM element nodes. Save the hooks information in the current tree.
  • In the function component context execution phase, and is in the process cycle plan whether update (didScheduleRenderPhaseUpdateDuringThisPass) rendering stage. The hooks are executed in turn, saving the hooks information in turn to the workInProgress tree (how to do that, more on later).
  • NextRenderLanes: Used to determine the priority.
  • Call Component(props, secondArg). This is where the function component is actually executed
  • No matter what kind of environment, ReactCurrentDispatcher. Current will be assigned. Initialization is given “HooksDispatcherOnMount” and updates are given “HooksDispatcherOnUpdate”.

HooksDispatcherOnMount & HooksDispatcherOnUpdate


const HooksDispatcherOnMount: Dispatcher = {

  readContext,

  useCallback: mountCallback,

  useContext: readContext,

  useEffect: mountEffect,

  useImperativeHandle: mountImperativeHandle,

  useLayoutEffect: mountLayoutEffect,

  useMemo: mountMemo,

  useReducer: mountReducer,

  useRef: mountRef,

  useState: mountState,

  useDebugValue: mountDebugValue,

  useDeferredValue: mountDeferredValue,

  useTransition: mountTransition,

  useMutableSource: mountMutableSource,

  useOpaqueIdentifier: mountOpaqueIdentifier,


  unstable_isNewReconciler: enableNewReconciler,

};


const HooksDispatcherOnUpdate: Dispatcher = {

  readContext,


  useCallback: updateCallback,

  useContext: readContext,

  useEffect: updateEffect,

  useImperativeHandle: updateImperativeHandle,

  useLayoutEffect: updateLayoutEffect,

  useMemo: updateMemo,

  useReducer: updateReducer,

  useRef: updateRef,

  useState: updateState,

  useDebugValue: updateDebugValue,

  useDeferredValue: updateDeferredValue,

  useTransition: updateTransition,

  useMutableSource: updateMutableSource,

  useOpaqueIdentifier: updateOpaqueIdentifier,


  unstable_isNewReconciler: enableNewReconciler,

};


Copy the code

So, a lot of things start to fall into place here.

  1. ‘renderWithHooks” ** **** ‘after calling, Dispatcher has it.
  2. React hooks Initialize and update. Initialize with mount XXX and update with Update XXX.

OK. So let’s do another picture.

calluseXXXWhat happened after that

useState

function mountWorkInProgressHook() :Hook {

  const hook: Hook = {

    memoizedState: null.baseState: null.baseQueue: null.queue: null.next: null};if (workInProgressHook === null) {

    // This is the first hook in the list

    currentlyRenderingFiber.memoizedState = workInProgressHook = hook;

  } else {

    // Append to the end of the list**workInProgressHook = workInProgressHook.next = hook; 六四屠杀/ / &

  }

  return workInProgressHook;

}


function mountState<S> (initialState: (() => S) | S,) :S.Dispatch<BasicStateAction<S> >]{

  const hook = mountWorkInProgressHook(); / / & 1

  if (typeof initialState === 'function') {

    // $FlowFixMe: Flow doesn't like mixed types

    initialState = initialState();

  }

  hook.memoizedState = hook.baseState = initialState;

  const queue = (hook.queue = { / / & 2

    pending: null.interleaved: null.lanes: NoLanes,

    dispatch: null.lastRenderedReducer: basicStateReducer,

    lastRenderedState: (initialState: any),

  });

  const dispatch: Dispatch< / / & 3

    BasicStateAction<S>,

  > = (queue.dispatch = (dispatchAction.bind(

    null,

    currentlyRenderingFiber,

    queue,

  ): any));

  return [hook.memoizedState, dispatch]; / / & 4

}

Copy the code

Analyze:

  1. Call mountWorkInProgressHook() to get a hook object. Source code intuitive look big, hook object has”memoizedState“,”baseState“,”queue“Attribute. bymountWorkInProgressHookThe internal implementation knows that eachhookIt’s all connected in a linked list and assigned toworkInProgressMemoizedState, thus further verifying function components usedmemoizedStatestorehooksA linked list.
  2. “Queue”. Update the queue
  3. Dispatch, it’s a verb.
  4. return [hook.memoizedState, dispatch]; Eventually return something that you can structure. ((const [XXX,setXxx] = useState())

If you look at it carefully, you’ll see that there are some properties that you don’t understand. What properties are stored in Hook objects?

  • MemoizedState: State information is stored in useState | useEffect holds effect objects | useMemo holds cached values and DEps | useRef holds ref objects.
  • BaseQueue: Stores the latest update queue in useState and useReducer.
  • BaseState: the latest state value generated during an update in usestate and useReducer.
  • Queue: Saves information about the pendingQueue to be updated and the update function dispatch.
  • Next: Points to the next hooks object.

Why can't you declare hooks in conditional statements like if

A: Because conditional statements break the list structure of the function component that stores hooks in memoizedState. Who do you want Next to point to?

Q: What is dispatchAction?

Answer: “mountState” exactly what appeal has been stated. But dispatchAction…

dispatchAction


function dispatchAction<S.A> (fiber: Fiber, queue: UpdateQueue
       
        , action: A,
       ,>) {

  if (__DEV__) {

    if (typeof arguments[3= = ='function') {

      console.error(

        "State updates from the useState() and useReducer() Hooks don't support the " +

          'second callback argument. To execute a side effect after ' +

          'rendering, declare it in the component body with useEffect().',); }}// &1 computes the render pass

  const eventTime = requestEventTime();

  const lane = requestUpdateLane(fiber);

 // &2 declare update

  const update: Update<S, A> = {

    lane,

    action,

    eagerReducer: null.eagerState: null.next: (null: any),

  };


  // Alternate refers to the current Fiber in the workInProgress tree corresponding Fiber

  const alternate = fiber.alternate; 

  

  // &3 Determine whether the current 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.

    // Used to be similar, right?

    **didScheduleRenderPhaseUpdateDuringThisPass** = didScheduleRenderPhaseUpdate = true;

    const pending = queue.pending;

    if (pending === null) {

      // This is the first update. Create a circular list.

      update.next = update;

    } else {

      update.next = pending.next;

      pending.next = update;

    }

    queue.pending = update;

  } else { 

  /* The current function component corresponds to *fiber* is not in the harmonic rendering phase **, ready to update **/*

  

  // &4 Determine whether to cross-update

    if (isInterleavedUpdate(fiber, lane)) {

      const interleaved = queue.interleaved;

      if (interleaved === null) {

        // This is the first update. Create a circular list.

        update.next = update;

        // At the end of the current render, this queue's interleaved updates will

        // be transferred to the pending queue.

        // &5 At the end of the current rendering, interleaved updates to this queue are transmitted to the pending queue.

        pushInterleavedQueue(queue);

      } else {

        update.next = interleaved.next;

        interleaved.next = update;

      }

      queue.interleaved = update;

    } else {

      const pending = queue.pending;

      if (pending === null) {

        // This is the first update. Create a circular list.

        update.next = update;

      } else {

        update.next = pending.next;

        pending.next = update;

      }

      queue.pending = update;

    }

    if (

      fiber.lanes === NoLanes &&

      (alternate === null || alternate.lanes === NoLanes)

    ) {

    / / & 6

    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);

        

          update.eagerReducer = lastRenderedReducer;

          update.eagerState = eagerState;

          / / & 7

          if (is(eagerState, currentState)) {

    

            return; }}// code... Omit part of the source code, write not to move.

      }

      // code... Omit part of the source code, write not to move.

    }

    // code... Omit part of the source code, write not to move.
、
    / / & 8

     const root = scheduleUpdateOnFiber(fiber, lane, eventTime);

     // code... Omit part of the source code, write not to move.

}
Copy the code

Analyze:

  1. Calculate the render channel and determine the priority to use
  2. The statement update
    1. Lane: No
    2. Action: No
    3. EagerReducer: Emergency reducer. A way to adjust priorities
    4. EagerState: used together
    5. Next: No explanation
  3. The second step of dispatchAction is to check whether the fiber object of the current function component is in the rendering phase. If it is in the rendering phase, we do not need to update the current function component, just update the current update fiber.lanes.
    1. Either a call to setState by a class component or a call to dispatchAction by a function component generates an Update object that records the update and then places the update in the pending queue.
  4. Determine whether to cross-update
  5. Staggered updates to this queue are transmitted to the pending queue at the end of the current rendering.
  6. If fiber is not currently in the update phase. Get the latest eagerState by calling lastRenderedReducer
  7. Shallow comparison with currentState, return if it’s equal.

This confirms why the useState component does not render when the two values are equal. This mechanism is different from setState in Component mode. ScheduleUpdateOnFiber is the main function for react rendering updates.

  1. ScheduleUpdateOnFiber specifies the rendering fiber

Q: Why is set XXX updated immediately when used with **** in **** non-React API?

A: The normal ones are slowed by the decelerator, and the abnormal ones are pending. But there is also the “Scheduler scheduling layer” involved. We are also in the Reconciler coordination layer. More on that later…

useEffect


function mountEffectImpl(fiberFlags, hookFlags, create, deps) :void {

  const hook = mountWorkInProgressHook();

  const nextDeps = deps === undefined ? null : deps;

  currentlyRenderingFiber.flags |= fiberFlags;

  **hook.memoizedState = pushEffect**(

    HookHasEffect | hookFlags,

    create,

    undefined,

    nextDeps,

  );

}


function mountEffect(

  create: () => (() => void) | void,

  deps: Array<mixed> | void | null.) :void {

  if (__DEV__) {

    // $FlowExpectedError - jest isn't a global, and isn't recognized outside of tests

    if ('undefined'! = =typeofjest) { warnIfNotCurrentlyActingEffectsInDEV(currentlyRenderingFiber); }}if( __DEV__ && enableStrictEffects && (currentlyRenderingFiber.mode & StrictEffectsMode) ! == NoMode ) {return mountEffectImpl(

      MountPassiveDevEffect | PassiveEffect | PassiveStaticEffect,

      HookPassive,

      create,

      deps,

    );

  } else {

    returnmountEffectImpl( PassiveEffect | PassiveStaticEffect, HookPassive, create, deps, ); }}Copy the code

Similarly, each hooks initialization creates a hook object, and then saves the current Effect Hook information in the hook’s “memoizedState”.

MountEffect and mountState’s “memoizedState” hold different things!

So, hook. MemoizedState = pushEffect what is pushEffect?


function pushEffect(tag, create, destroy, deps) {

  const effect: Effect = {

    tag,

    create,

    destroy,

    deps,

    // Circular

    next: (null: any),

  };

  / / & 1

  let componentUpdateQueue: null | FunctionComponentUpdateQueue = (currentlyRenderingFiber.updateQueue: any);

  if (componentUpdateQueue === null) { / / & 2

    componentUpdateQueue = createFunctionComponentUpdateQueue();

    currentlyRenderingFiber.updateQueue = (componentUpdateQueue: any);

    componentUpdateQueue.lastEffect = effect.next = effect;

  } else {

    const lastEffect = componentUpdateQueue.lastEffect;

    if (lastEffect === null) {

      componentUpdateQueue.lastEffect = effect.next = effect;

    } else {

      constfirstEffect = lastEffect.next; lastEffect.next = effect; effect.next = firstEffect; componentUpdateQueue.lastEffect = effect; }}/ / & 3

  return effect;

}
Copy the code

At this point you have a good idea of how to read the source code. I won’t comment on it… Suppress hit me)

Parsing the

  1. “PushEffect” creates an effect object and mounts updateQueue
  2. Determine if the component is rendering for the first time and create componentUpdate Ue, which is the Update ue for workInProgress. Effect is then placed in update Ue.
  3. Finally return “Effect”

Q: How does React perform all effects?

A: The Fiber object has an effectTag attribute. React implements depth-first traverse to facilitate Fiber trees. According to the Fiber object’s effectTag attribute, extract valid side effects of each Fiber object and construct an EffectList containing only side effects. The EffectList is finally iterated and the corresponding side effect callback is executed based on the effectTag attribute. (Curious about Fiber objects again?)

useMemo


function mountMemo<T> (

  nextCreate: () => T,

  deps: Array<mixed> | void | null.) :T {

  const hook = mountWorkInProgressHook();

  const nextDeps = deps === undefined ? null : deps;

  const nextValue = nextCreate();

  hook.memoizedState = [nextValue, nextDeps];

  return nextValue;

}


Copy the code

His look. Nothing complicated, just a memoizedState.

useRef


function mountRef<T> (initialValue: T) :{|current: T|} {

  const hook = mountWorkInProgressHook();

  if (enableUseRefAccessWarning) {

    // I'm sorry I left out some source code.

  } else {

    const ref = {current: initialValue};

    hook.memoizedState = ref;

    returnref; }}Copy the code

Actually, it’s simpler. The React version now adds an access warning that uses UseRef. If you’re interested, check it out…

updateWorkInProgressHook

If there’s a mount, there’s an update. React uses two sets of apis to care about initialization and updates, respectively. But if you look at the source code, the updateXXX code is incredibly simple…

Take a look at the source code.


function updateState<S> (initialState: (() => S) | S,) :S.Dispatch<BasicStateAction<S> >]{

  return updateReducer(basicStateReducer, (initialState: any));

}

function updateRefresh() {

  const hook = updateWorkInProgressHook();

  return hook.memoizedState;

}

function updateMemo<T> (

  nextCreate: () => T,

  deps: Array<mixed> | void | null.) :T {

  const hook = updateWorkInProgressHook();

  const nextDeps = deps === undefined ? null : deps;

  const prevState = hook.memoizedState;

  if(prevState ! = =null) {

    // Assume these are defined. If they're not, areHookInputsEqual will warn.

    if(nextDeps ! = =null) {

      const prevDeps: Array<mixed> | null = prevState[1];

      if (areHookInputsEqual(nextDeps, prevDeps)) {

        return prevState[0]; }}}const nextValue = nextCreate();

  hook.memoizedState = [nextValue, nextDeps];

  returnnextValue; }...Copy the code

Without further ado, you’ll notice that updateXX does about two things.

  1. Call updateWorkInProgressHook
  2. Update the hooks. MemoizedState

UpdateWorkInProgressHook updateWorkInProgressHook


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.

  let nextCurrentHook: null | Hook;

  if (currentHook === null) { // &1 Check if it is the first *hooks*

    const current = currentlyRenderingFiber.alternate;

    if(current ! = =null) {

      nextCurrentHook = current.memoizedState;

    } else {

      nextCurrentHook = null; }}else {

  

    nextCurrentHook = currentHook.next; // &2 No, then point to the next *hooks*

  }


  let nextWorkInProgressHook: null | Hook;

  if (workInProgressHook === null) { // &3 Determine whether to access *hooks* for the first time

    nextWorkInProgressHook = currentlyRenderingFiber.memoizedState;

  } else {

  

    nextWorkInProgressHook = workInProgressHook.next; // &4 If no, it points to......

  }


  if(nextWorkInProgressHook ! = =null) {

    // There's already a work-in-progress. Reuse it.

    // &5: a hook is being executed.

    workInProgressHook = nextWorkInProgressHook;

    nextWorkInProgressHook = workInProgressHook.next;


    currentHook = nextCurrentHook;

  } else {

    // Clone from the current hook.invariant( nextCurrentHook ! = =null.'Rendered more hooks than during the previous render.',); currentHook = nextCurrentHook;const newHook: Hook = { // &6 creates a new hook object

      memoizedState: currentHook.memoizedState,


      baseState: currentHook.baseState,

      baseQueue: currentHook.baseQueue,

      queue: currentHook.queue,

      next: null};if (workInProgressHook === null) {

      // &7 This is the first hook in the list.

      currentlyRenderingFiber.memoizedState = workInProgressHook = newHook;

    } else {

      // &8 Append to the end of the list.workInProgressHook = workInProgressHook.next = newHook; }}return workInProgressHook;

}
Copy the code

The subtotal

Ps: paste out of the source code I add notes can be carefully looked at, help to understand.

In fact, the source is also very simple to log? Just to recap, this may be helpful for your interview…