两条规则
React文档中介绍了React Hooks的如下使用规则。
只在最顶层使用 Hook
不要在循环,条件或嵌套函数中调用 Hook, 确保总是在你的 React 函数的最顶层调用他们。遵守这条规则,你就能确保 Hook 在每一次渲染中都按照同样的顺序被调用。这让 React 能够在多次的
useState
和useEffect
调用之间保持 hook 状态的正确。只在 React 函数中调用 Hook
不要在普通的 JavaScript 函数中调用 Hook。 你可以:
- ✅ 在 React 的函数组件中调用 Hook
- ✅ 在自定义 Hook 中调用其他 Hook
我们在使用hooks时需要遵循这两条规则,那么为什么有这两条限制呢?我们可以从源码中寻找答案。
Hooks基础
和class
组件的setState
方法类似,我们使用的hooks函数中都没有直接实现代码,而是调用一个叫做dispatcher
的对象中相应函数。源码地址
function resolveDispatcher() {
const dispatcher = ReactCurrentDispatcher.current;
return dispatcher;
}
export function useState<S>(
initialState: (() => S) | S,
): [S, Dispatch<BasicStateAction<S>>] {
const dispatcher = resolveDispatcher();
return dispatcher.useState(initialState);
}
export function useEffect(
create: () => (() => void) | void,
deps: Array<mixed> | void | null,
): void {
const dispatcher = resolveDispatcher();
return dispatcher.useEffect(create, deps);
}
...
函数组件是在 renderWithHooks 函数内执行的。
// 删除了与本文无关的代码
export function renderWithHooks<Props, SecondArg>(
current: Fiber | null,
workInProgress: Fiber,
Component: (p: Props, arg: SecondArg) => any,
props: Props,
secondArg: SecondArg,
nextRenderLanes: Lanes,
): any {
// workInProgress表示本次更新中函数组件对应的Fiber
currentlyRenderingFiber = workInProgress;
workInProgress.memoizedState = null;
workInProgress.updateQueue = null;
// current表示上次更新中函数组件对应的Fiber
// current === null 表示组件处于挂载阶段,否则处于更新阶段
// 不同阶段 ReactCurrentDispatcher.current等于不同的对象
ReactCurrentDispatcher.current =
current === null || current.memoizedState === null
? HooksDispatcherOnMount
: HooksDispatcherOnUpdate;
// 执行Component, Component即我们的函数组件
let children = Component(props, secondArg);
// 函数组件执行完,将ReactCurrentDispatcher.current 置为 ContextOnlyDispatcher
ReactCurrentDispatcher.current = ContextOnlyDispatcher;
currentlyRenderingFiber = null;
currentHook = null;
workInProgressHook = null;
return children;
}
// 挂载相关的dispatcher
const HooksDispatcherOnMount: Dispatcher = {
useCallback: mountCallback,
useContext: readContext,
useEffect: mountEffect,
useImperativeHandle: mountImperativeHandle,
useLayoutEffect: mountLayoutEffect,
useMemo: mountMemo,
useReducer: mountReducer,
useRef: mountRef,
useState: mountState,
...
};
// 更新相关的dispatcher
const HooksDispatcherOnUpdate: Dispatcher = {
useCallback: updateCallback,
useContext: readContext,
useEffect: updateEffect,
useImperativeHandle: updateImperativeHandle,
useLayoutEffect: updateLayoutEffect,
useMemo: updateMemo,
useReducer: updateReducer,
useRef: updateRef,
useState: updateState,
...
};
// 执行会抛错的dispatcher
export const ContextOnlyDispatcher: Dispatcher = {
useCallback: throwInvalidHookError,
useContext: throwInvalidHookError,
useEffect: throwInvalidHookError,
useImperativeHandle: throwInvalidHookError,
useLayoutEffect: throwInvalidHookError,
useMemo: throwInvalidHookError,
useReducer: throwInvalidHookError,
useRef: throwInvalidHookError,
useState: throwInvalidHookError,
...
};
为什么只在 React 函数组件中调用 Hook
以useState
为例,useState
内部会调用ReactCurrentDispatcher.current.useState
,由于只有在函数组件执行前才会将ReactCurrentDispatcher.current
设置为HooksDispatcherOnMount
或HooksDispatcherOnUpdate
,当函数组件执行完后ReactCurrentDispatcher.current
马上被设置为ContextOnlyDispatcher
,所以在 React 函数组件外使用useState
时,useState
内部会调用ContextOnlyDispatcher.useState
,该函数是会报错的。其他 hooks
同理。
为什么只在最顶层使用 Hook
每次调用 hooks
都会生成 hook
对象,其结构如下
export type Hook = {|
memoizedState: any,
baseState: any,
baseQueue: Update<any, any> | null,
queue: UpdateQueue<any, any> | null,
next: Hook | null,
|};
着重关注memoizedState
和next
属性,不同 hooks
的 hook
对象的memoizedState
保存着不同对象
useState
:hook.memoizedState = state;
useEffect
:hook.memoizedState = effect
useMemo
:hook.memoizedState = [nextValue, nextDeps];
useCallback
:hook.memoizedState = [callback, nextDeps];
useRef
:hook.memoizedState = ref;
一般我们会在函数组件中多次使用hooks
,这会产生多个 hook
对象,这些对象通过next
属性连接形成链表,连接是在挂载时进行的,主要函数是 mountWorkInProgressHook。
function mountWorkInProgressHook() {
const 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;
}
挂载时,hooks
都会调用该函数生成 hook
对象并连接。如果 workInProgressHook === null
,说明是hook
链中的第一个,将其赋值给currentlyRenderingFiber.memoizedState
并更新workInProgressHook
,这说明函数组件的Fiber的memoizedState
保存着第一个hook
的引用。当workInProgressHook !== null
,则将新生成的 hook
赋值给上个hook
的next
属性并更新workInProgressHook
。
更新时,hooks
都会调用 updateWorkInProgressHook 函数生成新的hook对象并连接形成hook链以供下次更新时使用。
function updateWorkInProgressHook(): Hook {
let nextCurrentHook: null | Hook;
if (currentHook === null) { // currentHook === null 说明当前是hook链的第一个
// current 为上次更新的组件对应的Fiber
const current = currentlyRenderingFiber.alternate;
if (current !== null) {
// current.memoizedState保存着hook链的第一个hook
nextCurrentHook = current.memoizedState;
} else {
nextCurrentHook = null;
}
} else {
// 取出 currentHook.next,即上次更新中hook链的下一个hook
nextCurrentHook = currentHook.next;
}
// 此时nextCurrentHook代表着本hooks在上次更新中的hook对象
let nextWorkInProgressHook: null | Hook;
if (workInProgressHook === null) { //workInProgressHook === null 说明当前是hook链的第一个
nextWorkInProgressHook = currentlyRenderingFiber.memoizedState;
} else {
nextWorkInProgressHook = workInProgressHook.next;
}
// 此时nextWorkInProgressHook绝大多数时候为null,因为 renderWithHooks 函数在执行函数组件前会将
// currentlyRenderingFiber.memoizedState置为null,workInProgressHook.next也为null
if (nextWorkInProgressHook !== null) { // 忽略这条分支
// There's already a work-in-progress. Reuse it.
workInProgressHook = nextWorkInProgressHook;
nextWorkInProgressHook = workInProgressHook.next;
currentHook = nextCurrentHook;
} else {
// 绝大多数时候会进入此分支
// currentHook即上次更新中相应顺序的hook对象
currentHook = nextCurrentHook;
// 复用上次更新的hook对象的属性,生成新的hook对象
const newHook: Hook = {
memoizedState: currentHook.memoizedState,
baseState: currentHook.baseState,
baseQueue: currentHook.baseQueue,
queue: currentHook.queue,
next: null,
};
//形成新的hook链,和挂载时一样
if (workInProgressHook === null) {
// This is the first hook in the list.
currentlyRenderingFiber.memoizedState = workInProgressHook = newHook;
} else {
// Append to the end of the list.
workInProgressHook = workInProgressHook.next = newHook;
}
}
return workInProgressHook;
}
正确使用hooks时,遍历hook链,复用hook上次更新的hook,没有问题,但如果某个hooks的调用与否是有条件的,那么情况就不同了。举个简单例子,如下使用hooks:
function FunComponent(props) {
if (props.condition) {
useEffect()
}
const [counter, setCounter] = useState(0)
const memo = useMemo()
}
假设第一次更新时props.condition === true
,那么 hook链是这样的:useEffect hook
=> useState hook
=>useMemo hook
。
第二次更新时props.condition === false
,我们走下更新过程。
useState
首先执行,它会调用updateWorkInProgressHook
函数,currentHook
为上次更新的hook
链的第一个hook
,也就是 useEffect hook
,然后复用该hook
属性得到新的hook
并 return 给 useState
使用,useState
再 return hook
对象的memoizedState
,但是此时的hook
对象的memoizedState
不是数字而是effect
对象,counter
从数字变成了对象,渲染或利用counter
计算时肯定会报错的。
同理,useMemo
执行也会调用updateWorkInProgressHook
函数,得到的是useState hook
,这个错误更严重,应为 useMemo
会把 hook
对象的 memoizedState
当做数组使用,此时的 hook.memoizedState
是个数字。看下更新时useMemo
的源码就明白了。
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) {
if (nextDeps !== null) {
// 上次的依赖
const prevDeps: Array<mixed> | null = prevState[1];
// 依赖没有变化
if (areHookInputsEqual(nextDeps, prevDeps)) {
return prevState[0];
}
}
}
const nextValue = nextCreate();
hook.memoizedState = [nextValue, nextDeps];
return nextValue;
}
总结
-
不要在普通的 JavaScript 函数中调用 Hook 的原因是在其他函数中使用的hooks函数实际上都是名为
throwInvalidHookError
的函数。 -
不要在循环,条件或嵌套函数中调用 Hook的原因是如果hooks的执行顺序发生变化会导致 hooks 中使用错误的
hook
对象。