这是React Hooks的首要规则,这是因为React Hooks 是以单向循环链表的形式存储,即是有序的。循环是为了从最后一个节点移到一个节点的时候,只需通过next一步就可以拿到第一个节点,而不需要一层层回溯。React Hooks的执行,分为 mount 和 update 阶段,在mount阶段的时候,通过mountWorkInProgressHook() 创建各个hooks (如useState, useMemo, useEffect, useCallback等),并且将当前hook添加到表尾。在update阶段,在获取或者更新hooks值的时候,会先获取当前hook的状态,hook.memoizedState,并且是按照顺序或读写更新hook,若在条件或者循环语句使用hooks,那么在更新阶段,若增加或者减少了某个hook,hooks的数量发生变化,而React是按照顺序,通过next读取下一个hook,则导致后面的hooks和挂载(mount)阶段对应不上,发生读写错值的情况,从而引发bug。
我们可以看看useState在mount阶段的源码:
function mountState<S>(
initialState: (() => S) | S,
): [S, Dispatch<BasicStateAction<S>>] {
const hook = mountWorkInProgressHook();
if (typeof initialState === 'function') {
// $FlowFixMe: Flow doesn't like mixed types
initialState = initialState();
}
hook.memoizedState = hook.baseState = initialState;
const queue: UpdateQueue<S, BasicStateAction<S>> = {
pending: null,
lanes: NoLanes,
dispatch: null,
lastRenderedReducer: basicStateReducer,
lastRenderedState: (initialState: any),
};
hook.queue = queue;
const dispatch: Dispatch<
BasicStateAction<S>,
> = (queue.dispatch = (dispatchSetState.bind(
null,
currentlyRenderingFiber,
queue,
): any));
return [hook.memoizedState, dispatch];
}
useCallback在mount阶段的源码:
function mountCallback<T>(callback: T, deps: Array<mixed> | void | null): T {
const hook = mountWorkInProgressHook();
const nextDeps = deps === undefined ? null : deps;
hook.memoizedState = [callback, nextDeps];
return callback;
}
然后mountWorkInProgressHook的源码如下:
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;
}
其他hooks的源码也是类似,以mountWorkInProgressHook创建当前hooks, 并且把hook的数据存到hook.memoizedState上,而在update阶段,则是依次读取hooks链表的memoizedState属性来获取状态 (数据)。
React 为什么要以单向循环链表的形式存储hooks呢?直接以key-value的对象形式存储就可以在循环或者条件语句中使用hooks了,岂不更好?
这是因为react scheduler的调度策略如此,以链表的形式存储是为了可以实现并发、可打断、可恢复、可继续执行下一个fiber任务。