Interpretation of React's source code and principles (11): the principle of hooks

Written at the beginning of the column (stack armor)

  1. The author is not an expert in front-end technology, but just a novice in front-end technology who likes to learn new things. He wants to learn source code just to cope with the rapidly declining front-end market and the need to find a job. This column is the author's own thinking in the process of learning And experience, there are also many parts that refer to other tutorials. If there are errors or problems, please point them out to the author. The author guarantees that the content is 100% correct. Please do not use this column as a reference answer.

  2. The reading of this column requires you to have a certain foundation of React, JavaScript and front-end engineering. The author will not explain many basic knowledge points, such as: what is babel, what is the syntax of jsx, please refer to it when necessary material.

  3. Many parts of this column refer to a large number of other tutorials. If there are similarities, the author plagiarized them. Therefore, this tutorial is completely open source. You can integrate, summarize and add your own understanding to various tutorials as the author.

Contents of this section

This chapter is relatively independent from the previous part. We will talk about the new feature Hooks we updated in React 16.8 . We all know that Hooks is mainly proposed to solve the state problem of function components. Before our class components had state, But function components do not, so in order to solve this problem, hooks were born. So in order to make our hooks hang in the entire React life cycle, how does React handle them, let's take a look at this part:

Definition of Hooks

First, let's take a look at the code of hooks. They are stored in /react/blob/main/packages/react/src/ReactHooks.js. They all use to resolveDispatcherinitialize one dispatcher , and then call the dispatcher corresponding method to realize their logic. inresolveDispatcher is dispatcher again ReactCurrentDispatcher.currentobtained from :

function resolveDispatcher() {
  const dispatcher = ReactCurrentDispatcher.current;
  return ((dispatcher: any): Dispatcher);
}

export function useContext<T>(Context: ReactContext<T>): T {
  const dispatcher = resolveDispatcher();
  if (__DEV__) {
  //....省略
  }
  return dispatcher.useContext(Context);
}

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

Back to renderWithHooks

About ReactCurrentDispatcher.current, we need to go back to the place where we first appeared hooks-related operations, that is, our renderWithHooksfunction , we omitted the relevant content at that time, now we need to go back and talk about this part of the content:

First of all, we see the beginning of renderWithHooksthe function current === null. We decide whether we use the initialized hooksDispatcher or the updated hooksDispatcher according to the judgment of . These two hooks are the Dispatcher we just saw in the function. The operation functions of various hooks we use

export function renderWithHooks<Props, SecondArg>(
  current: Fiber | null,
  workInProgress: Fiber,
  Component: (p: Props, arg: SecondArg) => any,
  props: Props,
  secondArg: SecondArg,
  nextRenderLanes: Lanes,
): any {
    
    
  // 获取当前函数组件对应的 fiber,并且初始化
  currentlyRenderingFiber = workInProgress;
  workInProgress.memoizedState = null;
  workInProgress.updateQueue = null;


  if (__DEV__) {
    
    
    // ....
  } else {
    
    
    // 根据当前阶段决定是初始化hook,还是更新hook
    ReactCurrentDispatcher.current =
      current === null || current.memoizedState === null
        ? HooksDispatcherOnMount
        : HooksDispatcherOnUpdate;
  }
  // 调用函数组件
  let children = Component(props, secondArg);
    
  // 重置hook链表指针
  currentHook = null;
  workInProgressHook = null;
    
  return children;
}

const HooksDispatcherOnMount: Dispatcher = {
    
    
  ...
  useCallback: mountCallback,
  useContext: readContext,
  useEffect: mountEffect,
  useImperativeHandle: mountImperativeHandle,
  useLayoutEffect: mountLayoutEffect,
  useMemo: mountMemo,
  useReducer: mountReducer,
  useRef: mountRef,
  useState: mountState,
  ...
};

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

Mount hooks during initialization

Then we look at our code logic based on the two phases we just had, Mount and Update. First, we mount our hooks during Mount. By looking at the functions in each, we can see that they all use HooksDispatcherOnMounta Let's mountWorkInProgressHooktake a look at the logic of this function:

  • First of all, it creates a hook. Its data structure has been clearly written in the comments. Each kind of hooks needs to store different values. We will talk about this in detail later, and also store the update queue of our hooks, which is similar to what we said above. The updateQueue of an article is consistent, so I won’t repeat it

  • workInProgressHookIt is the linked list of hooks in this function component. The hooks in a function component are stored together in the form of a linked list, and this linked list will be stored currentlyRenderingFiberin memoizedState.currentlyRenderingFiber Fiber

function mountState(initialState) {
    
    
  const hook = mountWorkInProgressHook();
  ...
}
  
function mountWorkInProgressHook(): Hook {
    
    
  const hook: Hook = {
    
    
    memoizedState: null, // 上次渲染时所用的 state
    baseState: null,  // 已处理的 update 计算出的 state
    baseQueue: null,  // 未处理的 update 队列(上一轮没有处理完成的)
    queue: null,      // 当前的 update 队列
    next: null, // 指向下一个hook
  };

  // 保存到链表中
  if (workInProgressHook === null) {
    
    
    currentlyRenderingFiber.memoizedState = workInProgressHook = hook;
  } else {
    
    
    workInProgressHook = workInProgressHook.next = hook;
  }
  return workInProgressHook;
}

Hooks when updating

After reading the hooks when mounting, let's take a look at the update, we still only look at the public part:

  • First of all, we know that our React has two trees, one is the Current tree to represent the currently displayed tree, and the other is the WorkInProgress tree to represent the current construction tree, so our hooks linked list is also divided into two currentHook and workInProgressHook
  • Now when we want to update a hooks, we first use nextCurrentHook and nextWorkInProgressHook to identify the next hooks that need to be operated. If nextWorkInProgressHook exists, we can use it directly, otherwise, we need to clone our hook from our nextCurrentHook and put it into it
function updateWorkInProgressHook(): Hook {
    
    

  // 获取 current 树上的 Fiber 的 hook 链表
  let nextCurrentHook: null | Hook;
  if (currentHook === null) {
    
    
    const current = currentlyRenderingFiber.alternate;
    if (current !== null) {
    
    
      nextCurrentHook = current.memoizedState;
    } else {
    
    
      nextCurrentHook = null;
    }
  } else {
    
    
    nextCurrentHook = currentHook.next;
  }

  // workInProgress 树上的 Fiber 的 hook 链表
  let nextWorkInProgressHook: null | Hook;
  if (workInProgressHook === null) {
    
    
    nextWorkInProgressHook = currentlyRenderingFiber.memoizedState;
  } else {
    
    
    nextWorkInProgressHook = workInProgressHook.next;
  }
    
  // 如果 nextWorkInProgressHook 不为空,直接使用
  if (nextWorkInProgressHook !== null) {
    
    
    workInProgressHook = nextWorkInProgressHook;
    nextWorkInProgressHook = workInProgressHook.next;
    currentHook = nextCurrentHook;
  } else {
    
    
    //否则我们克隆一份 hooks
    currentHook = nextCurrentHook;
    const newHook: Hook = {
    
    
      memoizedState: currentHook.memoizedState,
      baseState: currentHook.baseState,
      baseQueue: currentHook.baseQueue,
      queue: currentHook.queue,
      next: null,
    };
    if (workInProgressHook === null) {
    
    
      currentlyRenderingFiber.memoizedState = workInProgressHook = newHook;
    } else {
    
    
      workInProgressHook = workInProgressHook.next = newHook;
    }
  }
  return workInProgressHook;
}

Summary and expansion & classic interview questions

According to the above function, we can also see that when we update a hook, we need to traverse its Current tree and the hooks linked list of the WorkInProgress tree, so the order of hooks identified by its two linked lists must be consistent, which also leads to A common interview question - why do you need to put the hook function at the top level of the function component

Let's imagine a situation like this: if a hook is generated in a loop condition or an if statement, when we mount and when we update, the execution environment of the two times is different, resulting in different hooks or different orders generated twice , but we still traverse our two linked lists in the order we expected, and the final result is that we mistakenly reused another hook as the hook we currently need to reuse, resulting in unpredictable results. An example is as follows:

We set up two useStateand we make our first hook not execute after executing it once

let isMount = false;
if(!isMount) {
    
    
    const [num, setNum] = useState('value1'); // useState1
    isMount = true;
}
const [num2, setNum2] = useState('value2'); // useState2

Then the linked list we get when we call for the first time, that is, mount is: useState1 – useState2

Obviously our linked list when updating should be useState2

But according to our above code, when we update, we need to reuse a hook as our useState2. At this time, our nextCurrentHook points to the first hook of the linked list, which is useState1

In the end, the whole logic is messed up, so we have to keep in mind that the hook function needs to be placed at the top level of the function component, and Hooks cannot be called in loops, conditions or nested functions

The use and follow-up of hooks

We now know the logic of our hooks. In the next few articles, I hope to explain the principles of some commonly used hooks and the analysis of source code, which roughly include:

  • The most commonly used useState and useEffect
  • Performance optimization useMemo and useCallback
  • Get element useRef

It may also include some other content and custom hooks, you can continue to pay attention

Guess you like

Origin blog.csdn.net/weixin_46463785/article/details/130566356