写在专栏开头(叠甲)
-
作者并不是前端技术专家,也只是一名喜欢学习新东西的前端技术小白,想要学习源码只是为了应付急转直下的前端行情和找工作的需要,这篇专栏是作者学习的过程中自己的思考和体会,也有很多参考其他教程的部分,如果存在错误或者问题,欢迎向作者指出,作者保证内容 100% 正确,请不要将本专栏作为参考答案。
-
本专栏的阅读需要你具有一定的 React 基础、 JavaScript 基础和前端工程化的基础,作者并不会讲解很多基础的知识点,例如:babel 是什么,jsx 的语法是什么,需要时请自行查阅相关资料。
-
本专栏很多部分参考了大量其他教程,若有雷同,那是作者抄袭他们的,所以本教程完全开源,你可以当成作者对各类教程进行了整合、总结并加入了自己的理解。
本一节的内容
这个章节和前面的部分相对独立,我们将来讲一下我们在 React 16.8 更新的新特性 Hooks,我们都知道 Hooks 的提出主要就是为了解决函数组件的状态问题而存在的,之前我们的 class 组件有状态,但是 function 组件没有,所以为了解决这个问题,hooks 诞生了。那么为了让我们的 hooks 挂在在整个 React 的生命周期中, React 是怎么样处理他们的呢,我们来看看这部分的内容:
Hooks 的定义
首先我们来看看hooks 的代码,他们存放在 /react/blob/main/packages/react/src/ReactHooks.js 这个位置,他们都使用 resolveDispatcher
初始化了一个 dispatcher
,然后调用 dispatcher
对应的方法来实现他们的逻辑,而 resolveDispatcher
中的 dispatcher
又是从 ReactCurrentDispatcher.current
获取的:
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);
}
回到 renderWithHooks
关于 ReactCurrentDispatcher.current
,我们需要回到我们第一次出现 hooks 相关操作的位置,也就是我们的 renderWithHooks
函数,当时我们省略了相关的内容,现在我们需要回过来讲这部分的内容:
首先我们看到 renderWithHooks
函数的开始部分,我们根据 current === null
的判断来决定我们是使用初始化的 hooksDispatcher 还是更新的 hooksDispatcher,这两个 hooks 就是我们的刚刚看到的在函数中调用的 Dispatcher ,其中每一项都包含了我们用到的各种 hooks 的操作函数
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,
...
};
初始化时挂载 hooks
之后我们根据我们刚刚的两个阶段,Mount 和 Update 分别来看看我们的代码逻辑,首先是 Mount 时挂载我们的 hooks,通过查看每个HooksDispatcherOnMount
中的函数,我们可以看到,他们都使用了一个叫 mountWorkInProgressHook
的函数,我们来看看这个函数的逻辑:
-
首先它创建了一个 hook ,它的数据结构在注释已经写清楚了,每种 hooks 需要保存不一样的值,这个我们之后会详细来说,同时还会存放我们 hooks 的更新队列,这和我们上一篇的 updateQueue 是一致的,不再赘述
-
workInProgressHook
是这个函数组件中的 hooks 的链表,一个函数组件中的 hooks 的是以链表的形式保存在一起的,而这个链表会被保存在currentlyRenderingFiber
的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
看完了挂载时的 hooks ,我们再来看看更新,我们还是只看公共的部分:
- 首先我们知道,我们 React 是有两棵树的,一颗是 Current 树表示当前展示的树,一颗是 WorkInProgress 树表示当前的构建的树,那么我们的 hooks 链表也因此分为两个 currentHook 和 workInProgressHook
- 现在当我们要更新一个 hooks 的时候,我们首先使用 nextCurrentHook 和 nextWorkInProgressHook 来标识下一个需要操作的 hooks 。如果 nextWorkInProgressHook 存在,我们直接使用它即可,否则,我们需要从我们的 nextCurrentHook 处克隆一份我们的 hook 放入其中
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;
}
总结拓展 & 经典面试题
根据上面的函数我们也可以看到,当我们更新一个 hooks 的时候,我们需要遍历它的 Current 树和 WorkInProgress 树的 hooks 链表,所以它的两个链表标识的 hooks 顺序必须一致,这也引申出了一个我们常见的面试题 —— 为什么需要将hook函数放在函数组件的顶层
我们试想这样一种情况:如果有一个 hook 在循环条件或者 if 语句中产生,当我们挂载时和我们更新时,两次的执行环境不同导致了,导致了两次产生的 hook 不同或者顺序不同,但是我们还是按照我们预想的顺序对我们的两个链表进行遍历,最后导致的结果就是,我们错误的复用了另一个 hook 作为我们当前需要复用的 hook ,从而产生了不可预计的结果,一个例子如下:
我们设置两个 useState
我们让我们的第一个 hook 在执行一次后不再执行
let isMount = false;
if(!isMount) {
const [num, setNum] = useState('value1'); // useState1
isMount = true;
}
const [num2, setNum2] = useState('value2'); // useState2
那么我们在第一次调用 ,也就是 mount 的时候得到的链表是 : useState1 – useState2
显然我们在更新的时候的链表应该是 useState2
但是根据我们上文的代码,我们在更新的时候,需要复用一个 hook 作为我们的 useState2,此时我们的 nextCurrentHook 指向的是链表的第一个 hook 也就是 useState1
最后整个逻辑就乱套了,所以我们要牢记 需要将hook函数放在函数组件的顶层,不能在循环、条件或嵌套函数中调用 Hooks
hooks 的使用和后续
我们现在已经知道了我们 hooks 的逻辑,之后的几篇里面,我希望可以讲解一些常用的 hooks 的原理和源码的解析,大致包括:
- 最常用的 useState 和 useEffect
- 性能优化 useMemo 和 useCallback
- 获取元素 useRef
可能还包括一些其他的和自定义 hooks 的内容,大家可以持续关注