hooks不能在循环、条件或嵌套函数中调用
为什么?
// 20231120更新
我觉得还是先把答案说出来,再去看为什么比较合适。
其实就是一句话,因为函数组件渲染分两种场景
一、首次渲染
二、更新组件
原因就是,函数组件在首次渲染的时候,碰到hook语句时,是去创建一个空的hook对象挂载在memorizeState上,然后依次挂在next上。
而更新时,他不会重新依次创建空的hook对象,而是去上一次的memorizeState上按照顺序,依次去取hook对象并且赋值。
所以必须保证每次渲染执行函数组件时,hook语句不会因为什么别的条件而导致顺序或者数量变了。
带着疑问一起去看源码吧~
function App() {
const [num, setNum] = useState(0);
const [count, setCount] = useState(0);
const handleClick = () => {
setNum(num => num + 1)
setCount(2)
}
return <p onClick={() => handleClick()}>
{num}
{count}
</p>;
}
Fiber对象
想和大家一起回顾一下Fiber
React从V16开始就支持了hooks,引入了Fiber架构。
在之前的版本function 组件不能做继承,因为 function 本来就没这个特性,所以是提供了一些 api 供函数使用,这些 api 会在内部的一个数据结构上挂载一些函数和值,并执行相应的逻辑,通过这种方式实现了 state 和类似 class 组件的生命周期函数的功能,这种 api 就叫做 hooks,而hooks挂载数据的数据结构就是Fiber
classComponent,FunctionalComponent都会将节点信息存储在FIber对象中
{
type: any, // 对于类组件,它指向构造函数;对于DOM元素,它指定HTML tag
key: null | string, // 唯一标识符
stateNode: any, // 保存对组件的类实例,DOM节点或与fiber节点关联的其他React元素类型的引用
child: Fiber | null, // 大儿子
sibling: Fiber | null, // 下一个兄弟
return: Fiber | null, // 父节点
tag: WorkTag, // 定义fiber操作的类型, 详见https://github.com/facebook/react/blob/master/packages/react-reconciler/src/ReactWorkTags.js
nextEffect: Fiber | null, // 指向下一个节点的指针
alternate: Fiber | null,
updateQueue: mixed, // 用于状态更新,回调函数,DOM更新的队列
memoizedState: any, // 用于创建输出的fiber状态,记录内部state对象的属性
pendingProps: any, // 已从React元素中的新数据更新,并且需要应用于子组件或DOM元素的props
memoizedProps: any, // 在前一次渲染期间用于创建输出的props
// ……
}
memorizedState:单向链表
Fiber 对象的上有一个记录内部 State 对象的属性,以便让我们能在下次渲染的时候取到上一次的值,叫做 memoizedState
memorizedState将组件中的hooks依次链接在一起
即使知道他是链表,还是不知道为什么不能在条件里使用?
// 数据结构示例
fiber = {
// fiber 的 memorizedState 用于存储此函数组件的所有 hooks
// 在链表的 hooks 实现中就是指向第一个 useXxx 生成的 hook;数组实现中就是一个数组,第一个 hook 存储在索引0中。
memorizedState: hook1 { // 第一个 useXxx 生成的 hook
// useXxx 的数据
memorizedState: data,
// next 是个指针,指向下一个 useXxx 生成的 hook
next: hook2 {
// hook2 的数据
memorizedState: data,
// next 指向第三个 hook
next: hook3
}
}
}
updateQueue:单向链表[Effect类型对象]
是 Update 的队列,同时还带有更新的 dispatch。
const effect: Effect = {
tag,
create,
destroy,
deps,
// Circular
next: (null: any),
};
回顾完Fiber数据结构后,要开始进入正题啦
useState
源码部分
● currentlyRenderingFiber:指当前渲染组件的 Fiber 对象,在我们的例子中,就是 App 对应的 Fiber 对象
● workInProgressHook:指当前运行到哪个 hooks 了,我们一个组件内部可以有多个 hook,而当前运行的 hook 只有一个。
● currentFiber: 旧的Fiber节点
● workInProgress: 当前正在工作的Fiber节点
● hook 节点:我们每一个 useState 语句,在初始化的时候,都会产生一个对象,来记录它的状态,我们称它为 hook 节点。
export function useState<S>(
initialState: (() => S) | S,
): [S, Dispatch<BasicStateAction<S>>] {
const dispatcher = resolveDispatcher();
return dispatcher.useState(initialState);
}
function resolveDispatcher() {
const dispatcher = ReactCurrentDispatcher.current;
return ((dispatcher: any): Dispatcher);
}
export function renderWithHooks<Props, SecondArg>(
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;
// ……省略
}
从这里可以看出,我们的useState调用的函数分两种情况,mount和update,那么我们就分两个阶段来看源码
1.mount阶段
首次渲染
const HooksDispatcherOnMount: Dispatcher = {
readContext,
useCallback: mountCallback,
useContext: readContext,
useEffect: mountEffect,
useImperativeHandle: mountImperativeHandle,
useLayoutEffect: mountLayoutEffect,
useInsertionEffect: mountInsertionEffect,
useMemo: mountMemo,
useReducer: mountReducer,
useRef: mountRef,
useState: mountState,
useDebugValue: mountDebugValue,
useDeferredValue: mountDeferredValue,
useTransition: mountTransition,
useMutableSource: mountMutableSource,
useSyncExternalStore: mountSyncExternalStore,
useId: mountId,
};
mountState
- 创建 hook 对象,并将该 hook 对象加到 hook 链的末尾
- 初始化 hook 对象的状态值,也就是我们传进来的 initState 的值。
- 创建更新队列,这个队列是更新状态值的时候用的,会保存所有的更新行为。
- 绑定 dispatchSetState 函数
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;
// 声明一个链表来存放更新
// 用于多个 setState 的时候记录每次更新的。
const queue: UpdateQueue<S, BasicStateAction<S>> = {
pending: null,
lanes: NoLanes,
dispatch: null,
lastRenderedReducer: basicStateReducer,
lastRenderedState: (initialState: any),
};
hook.queue = queue;
// 返回一个dispatch方法用来修改状态,并将此次更新添加update链表中
const dispatch: Dispatch<BasicStateAction<S>> =
(queue.dispatch =
(dispatchSetState.bind(
null,
currentlyRenderingFiber,
queue): any)
);
// 返回当前状态和修改状态的方法
return [hook.memoizedState, dispatch];
}
1. mountWorkInProgressHook
会初始化创建一个 Hook,然后将其挂载到 workInProgress fiber 的 memoizedState 所指向的 hooks 链表上,以便于下次 update 的时候取出该 Hook:
- 创建一个hook节点
- 判断是否当前工作的hook节点workInProgressHook,没有的话,workInProgressHook = hook
- 有的话,workInProgressHook.next = hook, workInProgressHook = workInProgressHook.next
- 反正就是指针指向当前这个hook
function mountWorkInProgressHook(): Hook {
const hook: Hook = {
memoizedState: null,
baseState: null,
baseQueue: null, // 每次更新完会赋值上一个 update,方便 React 在渲染错误的边缘,数据回溯。
queue: null,
next: null,
};
if (workInProgressHook === null) {
// 当前workInProgressHook链表为空的话,
// 将当前Hook作为第一个Hook
// This is the first hook in the list
currentlyRenderingFiber.memoizedState = workInProgressHook = hook;
} else {
// Append to the end of the list
// 否则将当前Hook添加到Hook链表的末尾
workInProgressHook = workInProgressHook.next = hook;
}
return workInProgressHook;
}
每一个hook语句对应一个hook节点
2. mountWorkI15868169523dispatchSetState
useState 执行 setState 后会调用 dispatchSetState
- 创建update对象
- 将所有的 update 对象串成了一个环形链表,将update赋值给queue的pending属性上
function dispatchSetState<S, A>(
fiber: Fiber,
queue: UpdateQueue<S, A>,
action: A,
): void {
const lane = requestUpdateLane(fiber);
// 创建更新对象
const update: Update<S, A> = {
lane,
action, // 值
hasEagerState: false,
eagerState: null,
next: (null: any), //
};
if (isRenderPhaseUpdate(fiber)) { // fiber调度范畴
enqueueRenderPhaseUpdate(queue, update); // 缓存更新
} else {
const alternate = fiber.alternate;
if (
fiber.lanes === NoLanes &&
(alternate === null || alternate.lanes === NoLanes)
) {
const lastRenderedReducer = queue.lastRenderedReducer;
if (lastRenderedReducer !== null) {
let prevDispatcher;
try {
const currentState: S = (queue.lastRenderedState: any);
const eagerState = lastRenderedReducer(currentState, action);
update.hasEagerState = true;
update.eagerState = eagerState;
if (is(eagerState, currentState)) {
enqueueConcurrentHookUpdateAndEagerlyBailout(fiber, queue, update);
return;
}
} catch (error) {
// Suppress the error. It will throw again in the render phase.
}
}
}
const root = enqueueConcurrentHookUpdate(fiber, queue, update, lane);
if (root !== null) {
const eventTime = requestEventTime();
scheduleUpdateOnFiber(root, fiber, lane, eventTime);
entangleTransitionUpdate(root, queue, lane);
}
}
markUpdateInDevTools(fiber, lane, action);
}
function enqueueRenderPhaseUpdate<S, A>(
queue: UpdateQueue<S, A>,
update: Update<S, A>,
): void {
didScheduleRenderPhaseUpdateDuringThisPass = didScheduleRenderPhaseUpdate =
true;
const pending = queue.pending;
if (pending === null) {
// This is the first update. Create a circular list.
update.next = update;
} else {
update.next = pending.next;
pending.next = update;
}
queue.pending = update;
}
环形链表
初始的 update 对象,用来记录相关的 hook 信息,并将它添加到 queue 中,这里的 queue 的添加你可以发现它形成了一个循环链表,这样 pending 作为链表的一个尾结点,而 pending.next 就能够获取链表的头结点。这样做的目的是,在 setCount 时,我们需要将 update 添加到链表的尾部;而在下面的 updateReducer 中,我们需要获取链表的头结点来遍历链表,通过循环链表能够轻松实现我们的需求。
2. update阶段
updateState
function updateState<S>(
initialState: (() => S) | S,
): [S, Dispatch<BasicStateAction<S>>] {
return updateReducer(basicStateReducer, (initialState: any));
}
updateReducer
updateState 做的事情,实际上就是拿到更新队列,循环队列,并根据每一个 update 对象对当前 hook 进行状态更新,返回最终的结果
function updateReducer<S, I, A>(
reducer: (S, A) => S,
initialArg: I,
init?: I => S,
): [S, Dispatch<A>] {
const hook = updateWorkInProgressHook();
const queue = hook.queue;
queue.lastRenderedReducer = reducer; // 获取最近一次的reducer函数
const current: Hook = (currentHook: any);
let baseQueue = current.baseQueue; // 目前存在的state的更新链表
const pendingQueue = queue.pending; // 本次hook的state更新链表
// 把pendingQueue合并到baseQueue上
if (pendingQueue !== null)
if (baseQueue !== null) {
// 如果 baseQueue 和 pendingQueue 都存在,将 pendingQueue 链接到 baseQueue 尾部
const baseFirst = baseQueue.next;
const pendingFirst = pendingQueue.next;
baseQueue.next = pendingFirst;
pendingQueue.next = baseFirst;
}
current.baseQueue = baseQueue = pendingQueue;
queue.pending = null;
}
// 下次渲染执行到 updateState 阶段会取出 hook.queue,根据优先级确定最终的 state,最后返回来渲染。
if (baseQueue !== null) {
// We have a queue to process.
const first = baseQueue.next;
let newState = current.baseState;
// 如果当前的 update 优先级低于 render 优先级,下次 render 时再执行本次的 update
let newBaseState = null;
let newBaseQueueFirst = null;
let newBaseQueueLast: Update<S, A> | null = null;
let update = first;
do {
const updateLane = removeLanes(update.lane, OffscreenLane);
const isHiddenUpdate = updateLane !== update.lane;
const shouldSkipUpdate = isHiddenUpdate
? !isSubsetOfLanes(getWorkInProgressRootRenderLanes(), updateLane)
: !isSubsetOfLanes(renderLanes, updateLane);
// 如果当前的 update 优先级低于 render 优先级,下次 render 时再执行本次的 update
if (shouldSkipUpdate) {
// Priority is insufficient. Skip this update. If this is the first
// skipped update, the previous update/state is the new base
// update/state.
const clone: Update<S, A> = {
lane: updateLane,
action: update.action,
hasEagerState: update.hasEagerState,
eagerState: update.eagerState,
next: (null: any),
};
if (newBaseQueueLast === null) {
newBaseQueueFirst = newBaseQueueLast = clone;
newBaseState = newState;
} else {
newBaseQueueLast = newBaseQueueLast.next = clone;
}
currentlyRenderingFiber.lanes = mergeLanes(
currentlyRenderingFiber.lanes,
updateLane,
);
markSkippedUpdateLanes(updateLane);
} else {
if (newBaseQueueLast !== null) {
// newBaseQueueLast 不为 null,说明此前有跳过的 update
// update 之间可能存在依赖,将后续 update 都连接到 newBaseQueue 中留到下次 render 执行
const clone: Update<S, A> = {
lane: NoLane,
action: update.action,
hasEagerState: update.hasEagerState,
eagerState: update.eagerState,
next: (null: any),
};
newBaseQueueLast = newBaseQueueLast.next = clone;
}
const action = update.action;
if (update.hasEagerState) {
newState = ((update.eagerState: any): S);
} else {
// 根据 state 和 action 计算新的 state
newState = reducer(newState, action);
}
}
update = update.next;
} while (update !== null && update !== first);
if (newBaseQueueLast === null) {
// newBaseQueueLast 为 null,说明所有 update 处理完了,更新 baseState
newBaseState = newState;
} else {
// 未处理完留到下次执行
newBaseQueueLast.next = (newBaseQueueFirst: any);
}
// 如果新的 state 和之前的 state 不相等,标记需要更新
if (!is(newState, hook.memoizedState)) {
markWorkInProgressReceivedUpdate();
}
// 将新的 state 和 baseQueue 保存到 hook 中
hook.memoizedState = newState;
hook.baseState = newBaseState;
hook.baseQueue = newBaseQueueLast;
queue.lastRenderedState = newState;
}
if (baseQueue === null) {
queue.lanes = NoLanes;
}
const dispatch: Dispatch<A> = (queue.dispatch: any);
// 再次渲染的时候执行,会取出 hook.queue,根据优先级确定最终的 state 返回
return [hook.memoizedState, dispatch];
}
1. updateWorkInProgressHook
当 react 重新渲染时,会生成一个新的 fiber 树,而这里会根据之前已经生成的 FiberNode ,拿到之前的 hook ,再复制一份到新的 FiberNode 上,生成一个新的 hooks 链表。
而这个 hook 是怎么拿的?是去遍历 hooks 链表拿的,所以每次都会按顺序拿下一个 hook ,然后复制到新的 FiberNode 上。可以理解为这个 updateWorkInProgressHook 每次都会按顺序返回下一个 hook 。
nextCurrentHook,nextWorkInProgressHook两个hook对象分别对应的是oldFiber和当前workFiber
function updateWorkInProgressHook(): Hook {
let nextCurrentHook: null | Hook;
// currentHook: 已经生成的 fiber 树上的 hook,第一次是空
if (currentHook === null) {
// currentlyRenderingFiber$1: 正在生成的 FiberNode 结点, alternate 上挂载的是上一次已经生成完的 fiber 结点
// 所以 current 就是上次生成的 FiberNode
const current = currentlyRenderingFiber.alternate;
// memoizedState 是当前Fiber节点的hooks的链表信息
// 我们之前说过 hooks 挂在 FiberNode 的 memoizedState 上,这里拿到第一个 hook
if (current !== null) {
nextCurrentHook = current.memoizedState;
} else {
nextCurrentHook = null;
}
} else {
// 不是第一次,则证明已经拿到了 hook,我们只需要用 next 就能找到下一个 hook
nextCurrentHook = currentHook.next;
}
let nextWorkInProgressHook: null | Hook;
// workInProgressHook 当前运行到那个hook
// workInProgressHook: 正在生成的 FiberNode 结点上的 hook,第一次为空
if (workInProgressHook === null) {
// currentlyRenderingFiber$1 是当前正在生成的 FiberNode
// 所以这里 nextWorkInProgressHook 的值就是当前正在遍历的 hook,第一次让它等于 memoizedState
nextWorkInProgressHook = currentlyRenderingFiber.memoizedState;
} else {
// 不是第一次,始终让它指向下一个 hook,如果这是最后一个,那么 nextWorkInProgressHook 就会是 null
nextWorkInProgressHook = workInProgressHook.next;
}
if (nextWorkInProgressHook !== null) {
// There's already a work-in-progress. Reuse it.
// rerender场景下会走到这个逻辑,
workInProgressHook = nextWorkInProgressHook;
nextWorkInProgressHook = workInProgressHook.next;
currentHook = nextCurrentHook;
} else {
// Clone from the current hook.
// 不存在的话会根据上一次的 hook 克隆一个新的 hook,挂在新的链表、FiberNode上。
if (nextCurrentHook === null) {
const currentFiber = currentlyRenderingFiber.alternate;
if (currentFiber === null) {
// This is the initial render. This branch is reached when the component
// suspends, resumes, then renders an additional hook.
const newHook: Hook = {
memoizedState: null,
baseState: null,
baseQueue: null,
queue: null,
next: null,
};
nextCurrentHook = newHook;
} else {
// This is an update. We should always have a current hook.
throw new Error('Rendered more hooks than during the previous render.');
}
}
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;
}
workInProgressHook的伪代码
// 指向 hook 的指针
let workInProgressHook = null;
if (isMount) {
// useState, useEffect, useRef 这些 hooks 都是创建一个 hook 对象,然后用 memorizedState 存储 hook 的数据
hook = {
memorizedState: initState, // 当前 hook 数据
next: null, // 指向下一个 hook 的指针
}
if (!fiber.memorizedState) {
fiber.memorizedState = hook; // 不存在则是第一调用 useXxx,将 fiber.memorizedState 指向这第一个 hook
} else {
// fiber.memorizedState 存在则是多次调用 useXxx,将上个 hook 的 next 指向当前 hook
workInProgressHook.next = hook;
}
workInProgressHook = hook; // 存储当前 hook 用于下次使用
} else {
// workInProgressHook 是从第一个 hook 开始的,因为更新是通过 scheduler 来更新的,
// 而 scheduler 中对 workInProgressHook 进行了复位操作,即 workInProgressHook = fiber.memorizedState
// update 阶段,每个 useXxx 被调用的时候都会走 else 逻辑
hook = workInProgressHook;
// workInProgressHook 指向下一个 hook
workInProgressHook = hook.next;
}
useState 的 mountState 阶段返回的 setData是绑定了几个参数的 dispatch 函数。执行它会创建 hook.queue 记录更新,然后标记从当前到根节点的 fiber 的 lanes 和 childLanes 需要更新,然后调度下次渲染。
下次渲染执行到 updateState 阶段会取出 hook.queue,根据优先级确定最终的 state,最后返回来渲染。
最后用一哈别的大佬画的图~文章来源:https://www.toymoban.com/news/detail-443312.html
为什么?
看到这里你就应该明白为什么 hooks 只能在顶层使用了。核心在于updateWorkInProgressHook这个函数。
因为它会按顺序去拿hook,react也是按顺序来区分不同的 hook 的,它默认你不会修改这个顺序。如果你没有在顶层使用 hook ,打乱了每次 hook 调用的顺序,就会导致 react 无法区分出对应的 hook ,进而导致错误。文章来源地址https://www.toymoban.com/news/detail-443312.html
到了这里,关于为什么hooks不能在循环、条件或嵌套函数中调用的文章就介绍完了。如果您还想了解更多内容,请在右上角搜索TOY模板网以前的文章或继续浏览下面的相关文章,希望大家以后多多支持TOY模板网!