前言
在官网中的Hook 规则里,讲到了使用 Hook 需要遵循的两条规则:
- 不要在循环,条件或嵌套函数中调用 Hook。
- 只在 React 函数中调用 Hook。
接下来从源码看看到底为什么。
从 ReactHooks 的源码里可以看到 useState, useEffect,useRef,useMemo,useCallback...... 所有的 Hooks 里的代码都大同小异,都是调用 resolveDispatcher ,下面以 useState 为例。
为何不要在循环,条件或嵌套函数中调用 Hook?
export function useState(
initialState: (() => S) | S,
): [S, Dispatch>] {
const dispatcher = resolveDispatcher();
return dispatcher.useState(initialState);
}
function resolveDispatcher() {
const dispatcher = ReactCurrentDispatcher.current;
return ((dispatcher: any): Dispatcher);
}
可以看到 hooks 都是从 ReactCurrentDispatcher 上的 current 来的,初始化为null。(Dispatcher 里声明的,正好是一些 hooks 的类型)
const ReactCurrentDispatcher = {
/**
* @internal
* @type {ReactComponent}
*/
current: (null: null | Dispatcher),
};
问题:ReactCurrentDispatcher 上的 current 是在哪里被赋值的呢?
在 github 上搜索 ReactCurrentDispatcher 可以发现是在 ReactFiberHooks.new.js 文件里的 renderWithHooks 赋值的。(从名字上就可以感觉到这个和 fiber 架构和 hooks 有关了)
export function renderWithHooks(
current: Fiber | null,
workInProgress: Fiber,
Component: (p: Props, arg: SecondArg) => any,
props: Props,
secondArg: SecondArg,
nextRenderLanes: Lanes,
): any {
//......
ReactCurrentDispatcher.current =
current === null || current.memoizedState === null
? HooksDispatcherOnMount
: HooksDispatcherOnUpdate;
//......
}
可以看到初始化和更新的 hooks 是两套 api。
我们先只看mount 阶段的 HooksDispatcherOnMount
const HooksDispatcherOnMount: Dispatcher = {
readContext,
useCallback: mountCallback,
useContext: readContext,
useEffect: mountEffect,
useImperativeHandle: mountImperativeHandle,
useLayoutEffect: mountLayoutEffect,
useMemo: mountMemo,
useReducer: mountReducer,
useRef: mountRef,
useState: mountState,
useDebugValue: mountDebugValue,
useDeferredValue: mountDeferredValue,
useTransition: mountTransition,
useMutableSource: mountMutableSource,
useOpaqueIdentifier: mountOpaqueIdentifier,
unstable_isNewReconciler: enableNewReconciler,
};
useState 执行的自然就是 mountState。
function mountState(
initialState: (() => S) | S,
): [S, Dispatch>] {
const hook = mountWorkInProgressHook();
if (typeof initialState === 'function') {
initialState = initialState();
}
hook.memoizedState = hook.baseState = initialState;
//......
return [hook.memoizedState, dispatch];
}
function mountWorkInProgressHook(): Hook {
const hook: 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;
}
可以看到 mountWorkInProgressHook 里的代码,每次生成一个 hook 对象(里面保存了当前 hook 信息),然后通过 next 指向下一个 hook,以链表形式串联起来。
总结:hooks 的关系是通过 next 指向进行关联的,所以如果写在循环或条件语句中的话,next 的顺序就乱了,没有办法按照正确的顺序执行了。
为何只能在 React 函数中调用 Hook?
从上面我们已经可以知道,hooks 是在 renderWithHooks 内赋值的。
问题:renderWithHooks 又是在哪里被 import 使用了呢?
我们再来搜一下 renderWithHooks,可以看到是在 ReactFiberBeginWork.new.js 文件内被使用的 。(从文件的命名上,我们可以知道,这个是 fiber 开始执行的 js 文件)
function beginWork(
current: Fiber | null,
workInProgress: Fiber,
renderLanes: Lanes,
): Fiber | null {
//......
switch (workInProgress.tag) {
//......
case FunctionComponent: {
const Component = workInProgress.type;
const unresolvedProps = workInProgress.pendingProps;
const resolvedProps =
workInProgress.elementType === Component
? unresolvedProps
: resolveDefaultProps(Component, unresolvedProps);
return updateFunctionComponent(
current,
workInProgress,
Component,
resolvedProps,
renderLanes,
);
}
case ClassComponent: {
const Component = workInProgress.type;
const unresolvedProps = workInProgress.pendingProps;
const resolvedProps =
workInProgress.elementType === Component
? unresolvedProps
: resolveDefaultProps(Component, unresolvedProps);
return updateClassComponent(
current,
workInProgress,
Component,
resolvedProps,
renderLanes,
);
}
//......
}
//......
从这一段代码里我们可以知道,函数组件和 class 组件 调用的是不同的方法,而 updateFunctionComponent 里调用了 renderWithHooks 函数,updateClassComponent 没有调用。所以关于为什么的答案已经出来了。
总结一下:hooks 需要从 ReactCurrentDispatcher.current 上取得,而其赋值是在执行 renderWithHooks 时赋值的。执行函数组件的 updateFunctionComponent 内有调用 renderWithHooks 函数,执行 class 组件的 updateClassComponent 内没有调用其方法,所以使用 hooks 时必须写在函数组件内。
最后
希望这是加深对 hook 原理了解的第一步。