This is the 12th day of my participation in the August More Text Challenge. For details, see:August is more challenging

In the previous section on state and side effects, we summarized the class component, the Function component that uses the API to change the state and side effects of the Fiber node. For function component, its internal needs to rely on Hook to implement.

There is a special section in the official document to introduce Hook, and several concerned questions are copied here (please refer to the official website for other FAQs):

  1. The introduction ofHookMotivation?

    • It is difficult to reuse state logic between components; Complex components become difficult to understand; Unintelligible class. To address these practical development pain points, introducedHook.
  2. HookWhat is? When will it be usedHook?

    • HookIs a special function that lets you hook inReactFor example,useStateIs to allow you toReactFunction componentstateHook.
    • If you’re writing a function component and you realize you need to add something to itstate, the previous practice was to have to translate it intoclassNow you can use it in an existing function componentHook.
  3. HookWill it be slowed down by creating functions at render time?

    • No. In modern browsers, the raw performance of closures and classes differs significantly only in extreme scenarios. In addition, it can be argued thatHookThe design is more efficient in some ways:
      • HookTo avoid theclassAdditional overhead, such as the cost of creating class instances and binding event handlers in constructors.
      • Language custom code is in useHookDoes not require deep tree nesting of components. This phenomenon is in useHigh order component,render props, andcontextIs very common in the code base of The component tree is smaller,ReactThe amount of work is also reduced.

Therefore, Hook is the product of React team after a lot of practice. It is a more elegant replacement of class and has higher performance. Therefore, from the perspective of developers and users, they should embrace the convenience brought by Hook.

The hooks and Fiber

Through the explanation of the official website documents, I can quickly master the use of Hook. Combined with the previous introduction of state and side effects, we know that Hook is ultimately used to control the state and side effects of the fiber node. From the fiber perspective, the attributes associated with states and side effects are as follows (instead of explaining what individual attributes mean here, you can review states and side effects):

export type Fiber = {|
  // 1. The fiber node is related to its own state
  pendingProps: any,
  memoizedProps: any,
  updateQueue: mixed,
  memoizedState: any,

  // 2. The fiber node is related to the Effect
  flags: Flags,
  nextEffect: Fiber | null.firstEffect: Fiber | null.lastEffect: Fiber | null|};Copy the code

In the end, any API of the Hook is used to control these fiber properties.

Hook data structure

In ReactFiberHooks, you define the data structure for hooks:

type Update<S, A> = {|
  lane: Lane,
  action: A,
  eagerReducer: ((S, A) = > S) | null.eagerState: S | null.next: Update<S, A>, priority? : ReactPriorityLevel, |}; type UpdateQueue<S, A> = {| pending: Update<S, A> |null.dispatch: (A= > mixed) | null.lastRenderedReducer: ((S, A) = > S) | null.lastRenderedState: S | null|};export type Hook = {|
  memoizedState: any, // The current state
  baseState: any, / / the base state
  baseQueue: Update<any, any> | null./ / the queue
  queue: UpdateQueue<any, any> | null.// Update the queue
  next: Hook | null./ / next pointer
|};
Copy the code

By definition, a Hook object has five properties (the application of these properties will be analyzed in the Hook Principle (State) section). :

  1. hook.memoizedState: keeps a local state in memory.
  2. hook.baseState: hook.baseQueueAll of theupdateThe state of the merged object.
  3. hook.baseQueue: storageThe update objectOnly those that are higher in priority than this renderingThe update object.
  4. hook.queue: storageThe update objectThe circular linked list, including all priorityThe update object.
  5. hook.next: nextPointer to the next in the listhook.

Therefore, Hook is a linked list, and a single Hook has its own state Hook. MemoizedState and its own update queue Hook. Queue (the analysis of Hook state is explained in the Hook Principle (State) section).

Note: Hook.queue and fiber.updatequeue are both update circular linked lists, although the data structure and processing mode of update objects are highly similar. Hook. Queue is only used to maintain the status of hook objects and should not be confused with fiber.updatequeue.

Hook classification

In V17.0.2, 14 kinds of hooks are defined

export type HookType =
  | 'useState'
  | 'useReducer'
  | 'useContext'
  | 'useRef'
  | 'useEffect'
  | 'useLayoutEffect'
  | 'useCallback'
  | 'useMemo'
  | 'useImperativeHandle'
  | 'useDebugValue'
  | 'useDeferredValue'
  | 'useTransition'
  | 'useMutableSource'
  | 'useOpaqueIdentifier';
Copy the code

The official website has divided them into two categories, State Hook and Effect Hook.

Here, we can understand the difference between state Hook and side effect Hook from the perspective of fiber by combining the previous state and side effect Hook.

State the hooks

In a narrow sense, useState, useReducer can add internal state in the function component, and useState is actually a simple encapsulation of useReducer, which is one of the most special (simple) useReducer. So useState, useReducer is called status Hook.

Broadly speaking, as long as the Hook can achieve data persistence and no side effects, can be regarded as state Hook, so also include useContext, useRef, useCallback, useMemo and so on. Such hooks do not use useState/useReduer internally, but they can also implement render multiple times, leaving their initial values unchanged (that is, data persistence) without any side effects.

Thanks to double buffering technology, fiber is taken as the carrier to ensure the reuse of the same Hook object in multiple render, thus realizing data persistence. The specific implementation details are discussed in the Hook principle (state) section.

Side effects of Hook

Back to the fiber perspective, the state Hook implements state persistence (equivalent to class component maintaining fiber.memoizedState), while the side Hook modifiers fiber.flags. We know that in the performUnitOfWork->completeWork phase, all fiber nodes with side effects will be added to the parent node’s side effect queue and finally processed in the commitRoot phase.

In addition, side effect hooks also provide side effect callbacks (similar to life cycle callbacks for class components), such as:

// To use useEffect, you need to pass a side effect callback function.
// After the fiber tree is constructed, the commitRoot phase processes these side effect callbacks
useEffect(() = > {
  console.log('This is a side effect callback.'); } []);Copy the code

UseEffect is the standard side effect Hook inside react. Others, such as useLayoutEffect and custom hooks, must be called useEffect directly or indirectly if side effects are to be implemented.

The useEffect implementation details are discussed in the Hook Principles (Side effects) section.

Combination of Hook

Although there is no expression of combination Hook on the official website, in fact, most hooks (including custom hooks) are composed of the above two kinds of hooks, and have the characteristics of the two kinds of hooks.

  • inreactinternaluseDeferredValue, useTransition, useMutableSource, useOpaqueIdentifierAnd so on.
  • In normal development,Customize the HookMost of them are combination hooks.

Here’s an example of a custom Hook on the official website:

import { useState, useEffect } from 'react';

function useFriendStatus(friendID) {
  // 1. Call useState to create a status Hook
  const [isOnline, setIsOnline] = useState(null);

  // 2. Call useEffect and create a side effect Hook
  useEffect(() = > {
    function handleStatusChange(status) {
      setIsOnline(status.isOnline);
    }
    ChatAPI.subscribeToFriendStatus(friendID, handleStatusChange);
    return () = > {
      ChatAPI.unsubscribeFromFriendStatus(friendID, handleStatusChange);
    };
  });
  return isOnline;
}
Copy the code

Call the function before

Before calling function, react needs to do some preparatory work internally.

The processing function

From the perspective of fiber tree construction, different types of fiber simply require different handler functions to be called to return fiber subnodes. So in performUnitOfWork->beginWork function, calls a variety of processing functions. From the caller’s point of view, you don’t have to worry about the internal implementation of the handler (for example, Hook objects are used inside updateFunctionComponent, class instances are used inside updateClassComponent).

This section discusses hooks, so the updateFunctionComponent function is listed:

// Keep only FunctionComponent relevant:
function beginWork(
  current: Fiber | null,
  workInProgress: Fiber,
  renderLanes: Lanes,
) :Fiber | null {
  const updateLanes = workInProgress.lanes;
  switch (workInProgress.tag) {
    case FunctionComponent: {
      const Component = workInProgress.type;
      const unresolvedProps = workInProgress.pendingProps;
      const resolvedProps =
        workInProgress.elementType === Component
          ? unresolvedProps
          : resolveDefaultProps(Component, unresolvedProps);
      returnupdateFunctionComponent( current, workInProgress, Component, resolvedProps, renderLanes, ); }}}function updateFunctionComponent(current, workInProgress, Component, nextProps: any, renderLanes,) {
  / /... Omit irrelevant code
  let context;
  let nextChildren;
  prepareToReadContext(workInProgress, renderLanes);

  // Enter the Hooks logic, and finally return the parent ReactElement object
  nextChildren = renderWithHooks(
    current,
    workInProgress,
    Component,
    nextProps,
    context,
    renderLanes,
  );
  // Switch to the Reconcile function and generate a subordinate fiber node
  reconcileChildren(current, workInProgress, nextChildren, renderLanes);
  // Return the sub-fiber node
  return workInProgress.child;
}
Copy the code

RenderWithHooks (located in ReactFiberHooks) are called in the updateFunctionComponent function, and at this point Fiber is associated with hooks.

The global variable

Before analyzing the renderWithHooks function, it is important to understand the global variables defined in the ReactFiberHooks header (annotating them in English in the source code):

// Render priority
let renderLanes: Lanes = NoLanes;

// The fiber currently being constructed is equivalent to workInProgress, so it is renamed to distinguish it from the current hook
let currentlyRenderingFiber: Fiber = (null: any);

// Hooks are stored on the fiber.memoizedState list
let currentHook: Hook | null = null; // currentHook = fiber(current).memoizedState

let workInProgressHook: Hook | null = null; // workInProgressHook = fiber(workInProgress).memoizedState

// Whether the update was initiated again during the execution of the function. The function is reset only after it has been fully executed.
// This variable is used to determine whether to clear updates to render when it is abnormal.
let didScheduleRenderPhaseUpdate: boolean = false;

// Whether the update was initiated again during the execution of the function. Function is reset every time it is called
let didScheduleRenderPhaseUpdateDuringThisPass: boolean = false;

// The maximum number of updates to be reinitiated during the execution of this function
const RE_RENDER_LIMIT = 25;
Copy the code

The explanation of each variable can be compared with the English comments in the source code, among which the most important are:

  1. currentlyRenderingFiber: Fiber currently under construction, equivalent to workInProgress
  2. CurrentHook and workInProgressHook: Respectively pointing tocurrent.memoizedStateandworkInProgress.memoizedState

Note: Please review double buffering for the difference between current and workInProgress.

RenderWithHooks function

RenderWithHooks source code looks long, but the logic is clear when the main bar is left after removing dev. With the function call as the cut-off point, the logic is divided into three parts:

/ /... Omit irrelevant code
export function renderWithHooks<Props.SecondArg> (
  current: Fiber | null,
  workInProgress: Fiber,
  Component: (p: Props, arg: SecondArg) => any,
  props: Props,
  secondArg: SecondArg,
  nextRenderLanes: Lanes,
) :any {
  // --------------- 1. Set the global variable -------------------
  renderLanes = nextRenderLanes; // The current render priority
  currentlyRenderingFiber = workInProgress; // The current fiber node is the fiber node corresponding to the function component

  // Clear the remaining state of the current fiber
  workInProgress.memoizedState = null;
  workInProgress.updateQueue = null;
  workInProgress.lanes = NoLanes;

  / / -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- 2. Call the function, to generate the child ReactElement object -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- --
  // Specify a dispatcher to distinguish between mount and update
  ReactCurrentDispatcher.current =
    current === null || current.memoizedState === null
      ? HooksDispatcherOnMount
      : HooksDispatcherOnUpdate;
  // Execute the function, which analyzes the use of Hooks
  let children = Component(props, secondArg);

  / / -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- 3. Reset the global variables, and return -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- --
  // After function is executed, restore the modified global variables without affecting the next call
  renderLanes = NoLanes;
  currentlyRenderingFiber = (null: any);

  currentHook = null;
  workInProgressHook = null;
  didScheduleRenderPhaseUpdate = false;

  return children;
}
Copy the code
  1. callfunctionBefore: set global variable, flagRender priorityAnd the currentfiber, clear the currentfiberThe legacy state of.
  2. callfunctionIs constructed:HooksLinked list, and finally generate childrenReactElementObject (children).
  3. callfunctionAfter: reset the global variable, returnchildren.
    • In order to make sure that it’s differentfunctionNode when invokedrenderWithHooksDo not affect each other, so reset the global variable on exit.

Call the function

Hooks structure

In function, if Hook API is used (e.g. UseEffect, useState), a corresponding Hook object will be created. Next, we will focus on the creation process of Hook object.

Has the following demo:

import { useState, useEffect } from 'react';
export default function App() {
  // 1. useState
  const [a, setA] = useState(1);
  // 2. useEffect
  useEffect(() = > {
    console.log(`effect 1 created`);
  });
  // 3. useState
  const [b] = useState(2);
  // 4. useEffect
  useEffect(() = > {
    console.log(`effect 2 created`);
  });
  return (
    <>
      <button onClick={()= > setA(a + 1)}>{a}</button>
      <button>{b}</button>
    </>
  );
}
Copy the code

In the function component, both the status Hook and the side effect Hook are used.

}}}}}}}}}}}}}}}}}}}}}}}}}}}}}}}}}}}}}}}}}}}}}}}}}}}}}}}}}}}}}}}}}}}}}} The fiber tree construction process can be reviewed in the previous article):

Start calling function when renderWithHooks are executed. In this example, the Hook API is used four times inside the function, calling useState, useEffect, useState, useEffect in sequence.

While useState and useEffect correspond to mountState and mountEffect->mountEffectImpl respectively in the initial construction of fiber

function mountState<S> (
  initialState: (() => S) | S,
) :S.Dispatch<BasicStateAction<S> >]{
  const hook = mountWorkInProgressHook();
  / /... Omissions are not discussed in this section
  return [hook.memoizedState, dispatch];
}

function mountEffectImpl(fiberFlags, hookFlags, create, deps) :void {
  const hook = mountWorkInProgressHook();
  / /... Omissions are not discussed in this section
}
Copy the code

Both useState and useEffect create a hook internally via mountWorkInProgressHook.

Linked list to store

And mountWorkInProgressHook is very simple:

function mountWorkInProgressHook() :Hook {
  const hook: Hook = {
    memoizedState: null.baseState: null.baseQueue: null.queue: null.next: null};if (workInProgressHook === null) {
    // The first hook in the list
    currentlyRenderingFiber.memoizedState = workInProgressHook = hook;
  } else {
    // Add hook to the end of the list
    workInProgressHook = workInProgressHook.next = hook;
  }
  return workInProgressHook;
}
Copy the code

The logic is to create hooks and mount them on fiber.memoizedState, with multiple hooks saved in a linked list structure.

In this example, 4 hooks will be created after the function call, and the memory structure is as follows:

As you can see, both state hooks and side hooks are stored in the fiber.memoizedState linked list in the order they are called.

Order of cloning

Fiber-tree (); fiber-tree (); fiber-tree (); fiber-tree (); fiber-tree ();

Note: in renderWithHooks function has already been set up in workInProgress. MemoizedState = null, waiting for the call function reset.

Next call function, again useState, useEffect, useState, useEffect. While useState and useEffect correspond to updateState->updateReducer and updateEffect->updateEffectImpl respectively in fiber comparison

// ----- status Hook --------
function updateReducer<S.I.A> (reducer: (S, A) => S, initialArg: I, init? : I => S,) :S.Dispatch<A>] {
  const hook = updateWorkInProgressHook();
  / /... Omissions are not discussed in this section
}

// ----- side effect Hook --------
function updateEffectImpl(fiberFlags, hookFlags, create, deps) :void {
  const hook = updateWorkInProgressHook();
  / /... Omissions are not discussed in this section
}
Copy the code

Call updateWorkInProgressHook internally to get a hook regardless of useState, useEffect.

function updateWorkInProgressHook() :Hook {
  // 1. Move currentHook
  let nextCurrentHook: null | Hook;
  if (currentHook === null) {
    const current = currentlyRenderingFiber.alternate;
    if(current ! = =null) {
      nextCurrentHook = current.memoizedState;
    } else {
      nextCurrentHook = null; }}else {
    nextCurrentHook = currentHook.next;
  }

  // 2. Move the workInProgressHook pointer
  let nextWorkInProgressHook: null | Hook;
  if (workInProgressHook === null) {
    nextWorkInProgressHook = currentlyRenderingFiber.memoizedState;
  } else {
    nextWorkInProgressHook = workInProgressHook.next;
  }

  if(nextWorkInProgressHook ! = =null) {
    // Update at render: not covered in this section
  } else {
    currentHook = nextCurrentHook;
    // 3. Clone currentHook as the new workInProgressHook.
    // The following logic is the same as mountWorkInProgressHook
    const newHook: Hook = {
      memoizedState: currentHook.memoizedState,

      baseState: currentHook.baseState,
      baseQueue: currentHook.baseQueue,
      queue: currentHook.queue,

      next: null.// Note that the next pointer is null
    };
    if (workInProgressHook === null) {
      currentlyRenderingFiber.memoizedState = workInProgressHook = newHook;
    } else{ workInProgressHook = workInProgressHook.next = newHook; }}return workInProgressHook;
}
Copy the code

The updateWorkInProgressHook function is logically simple: the purpose is to move the currentHook and workInProgressHook Pointers backward simultaneously.

  1. Due to theRenderWithHooks functionSet upworkInProgress.memoizedState=null, soworkInProgressThe initial value must benull, only fromcurrentHookCloning.
  2. And from acurrentHookIt’s a clonenewHook.next=nullAs a result,workInProgressHookThe list needs to be completely rebuilt.

So after the function completes, the memory structure of the Hook is as follows:

You can see:

  1. Based on double buffering technology, willcurrent.memoizedStateCloned in orderworkInProgress.memoizedStateIn the.
  2. HookAfter a clone, the internal properties (hook.memoizedStateEtc.), so its state is not lost.

conclusion

This section first introduces the explanation of Hook in the official document to understand the origin of Hook and the advantages of Hook compared with class. Then the intrinsic relationship between fiber and hook is analyzed from the perspective of fiber. Hook list is mounted on fiber. MemoizedState by means of renderWithHooks function. The double buffering technology in fiber tree is used to realize the transfer of Hook from current to workInProgress, and then the persistence of Hook state is realized.

Write in the last

This article belongs to the diagram react source code in the state management plate, this series of nearly 20 articles, really in order to understand the React source code, and then improve the architecture and coding ability.

The first draft of the graphic section has been completed and will be updated in August. If there are any errors in the article, we will correct them as soon as possible on Github.