Written at the beginning of the column (stack armor)
-
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.
-
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.
-
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 resolveDispatcher
initialize one dispatcher
, and then call the dispatcher
corresponding method to realize their logic. inresolveDispatcher
is dispatcher
again ReactCurrentDispatcher.current
obtained 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 renderWithHooks
function , 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 renderWithHooks
the 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 HooksDispatcherOnMount
a Let's mountWorkInProgressHook
take 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
-
workInProgressHook
It 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 storedcurrentlyRenderingFiber
inmemoizedState
.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 useState
and 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