为什么hooks不能在循环、条件或嵌套函数中调用

news/2024/10/21 3:08:54/

hooks不能在循环、条件或嵌套函数中调用

在这里插入图片描述

为什么?

带着疑问一起去看源码吧~

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 tagkey: 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.jsnextEffect: Fiber | null, // 指向下一个节点的指针alternate: Fiber | null,updateQueue: mixed, // 用于状态更新,回调函数,DOM更新的队列memoizedState: any, // 用于创建输出的fiber状态,记录内部state对象的属性pendingProps: any, // 已从React元素中的新数据更新,并且需要应用于子组件或DOM元素的propsmemoizedProps: 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 生成的 hooknext: hook2 {// hook2 的数据memorizedState: data,// next 指向第三个 hooknext: hook3}}
}
updateQueue:单向链表[Effect类型对象]

是 Update 的队列,同时还带有更新的 dispatch。

const effect: Effect = {tag,create,destroy,deps,// Circularnext: (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
  1. 创建 hook 对象,并将该 hook 对象加到 hook 链的末尾
  2. 初始化 hook 对象的状态值,也就是我们传进来的 initState 的值。
  3. 创建更新队列,这个队列是更新状态值的时候用的,会保存所有的更新行为。
  4. 绑定 dispatchSetState 函数
function mountState<S>(initialState: (() => S) | S,
): [S, Dispatch<BasicStateAction<S>>] {const hook = mountWorkInProgressHook();if (typeof initialState === 'function') {// $FlowFixMe: Flow doesn't like mixed typesinitialState = 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:

  1. 创建一个hook节点
  2. 判断是否当前工作的hook节点workInProgressHook,没有的话,workInProgressHook = hook
  3. 有的话,workInProgressHook.next = hook, workInProgressHook = workInProgressHook.next
  4. 反正就是指针指向当前这个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 listcurrentlyRenderingFiber.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

  1. 创建update对象
  2. 将所有的 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 时再执行本次的 updatelet 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 时再执行本次的 updateif (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 计算新的 statenewState = reducer(newState, action);}}update = update.next;} while (update !== null && update !== first);if (newBaseQueueLast === null) {// newBaseQueueLast 为 null,说明所有 update 处理完了,更新 baseStatenewBaseState = 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 就是上次生成的 FiberNodeconst current = currentlyRenderingFiber.alternate;//  memoizedState 是当前Fiber节点的hooks的链表信息// 我们之前说过 hooks 挂在 FiberNode 的 memoizedState 上,这里拿到第一个 hookif (current !== null) {nextCurrentHook = current.memoizedState;} else {nextCurrentHook = null;}} else {// 不是第一次,则证明已经拿到了 hook,我们只需要用 next 就能找到下一个 hooknextCurrentHook = currentHook.next;}let nextWorkInProgressHook: null | Hook;// workInProgressHook 当前运行到那个hook// workInProgressHook: 正在生成的 FiberNode 结点上的 hook,第一次为空if (workInProgressHook === null) {// currentlyRenderingFiber$1 是当前正在生成的 FiberNode// 所以这里 nextWorkInProgressHook 的值就是当前正在遍历的 hook,第一次让它等于 memoizedStatenextWorkInProgressHook = currentlyRenderingFiber.memoizedState;} else {// 不是第一次,始终让它指向下一个 hook,如果这是最后一个,那么 nextWorkInProgressHook 就会是 nullnextWorkInProgressHook = 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 指向当前 hookworkInProgressHook.next = hook;}workInProgressHook = hook;  // 存储当前 hook 用于下次使用
} else {// workInProgressHook 是从第一个 hook 开始的,因为更新是通过 scheduler 来更新的,// 而 scheduler 中对 workInProgressHook 进行了复位操作,即 workInProgressHook = fiber.memorizedState// update 阶段,每个 useXxx 被调用的时候都会走 else 逻辑hook = workInProgressHook;// workInProgressHook 指向下一个 hookworkInProgressHook = hook.next;
}

useState 的 mountState 阶段返回的 setData是绑定了几个参数的 dispatch 函数。执行它会创建 hook.queue 记录更新,然后标记从当前到根节点的 fiber 的 lanes 和 childLanes 需要更新,然后调度下次渲染。
下次渲染执行到 updateState 阶段会取出 hook.queue,根据优先级确定最终的 state,最后返回来渲染。

最后用一哈别的大佬画的图~
在这里插入图片描述

为什么?

看到这里你就应该明白为什么 hooks 只能在顶层使用了。核心在于updateWorkInProgressHook这个函数。
因为它会按顺序去拿hook,react也是按顺序来区分不同的 hook 的,它默认你不会修改这个顺序。如果你没有在顶层使用 hook ,打乱了每次 hook 调用的顺序,就会导致 react 无法区分出对应的 hook ,进而导致错误。


http://www.ppmy.cn/news/66125.html

相关文章

AcWing算法提高课-1.3.9庆功会

宣传一下算法提高课整理 <— CSDN个人主页&#xff1a;更好的阅读体验 <— 本题链接&#xff08;AcWing&#xff09; 点这里 题目描述 为了庆贺班级在校运动会上取得全校第一名成绩&#xff0c;班主任决定开一场庆功会&#xff0c;为此拨款购买奖品犒劳运动员。 期望…

5.QT应用程序主窗口

本章代码见文末链接 主窗口框架 新建Qt Wisgets项目mymainwindow&#xff0c;类名默认MainWindow&#xff0c;基类默认QMainWindow 更改文字如图&#xff0c;如果中文无法直接输入&#xff0c;可以试试复制粘贴 “动作编辑器”中&#xff08;默认在右下角&#xff09;&…

一个功能强大的嵌入式shell命令

letter shell 3.x [外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-BvzKLv3I-1683945465686)(null)] [外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-LsLusHg6-1683945466009)(null)] [外链图片转存失败,源站可能有防盗链机…

注入攻击(二)--------HTML(有源码)

前序文章 注入攻击&#xff08;一&#xff09;--------SQL注入(结合BUUCTF sqli-labs) 目录 示例网站搭建1.搭建LAMP开发环境1. MySQL2. PHP3. Apache 写在示例前示例1.反射型HTML注入页面效果源码 示例2.钓鱼表单页面效果源码 示例3.存储型HTML注入页面效果源码 示例网站搭建 …

Python——pyqt5的计算器(源码+打包)

目录 一、效果图 二、源码 三、如何打包 四、如何减小打包程序大小&#xff08;方法1&#xff09; 五、如何减小打包程序大小&#xff08;方法2&#xff09; 学习视频 一、效果图 只是单纯的练手&#xff0c;然后再学习一下如何打包 二、源码 calculator_UI.zip - 蓝奏云…

7.外观模式C++用法示例

外观模式 一.外观模式1.原理2.特点3.外观模式与装饰器模式的异同4.应用场景C程序示例 一.外观模式 外观模式&#xff08;Facade Pattern&#xff09;是一种结构型设计模式&#xff0c;它提供了一个简单的接口&#xff0c;隐藏了一个或多个复杂的子系统的复杂性&#xff0c;并使…

宏任务、微任务在时间循环中的执行

如何描述事件循环&#xff0c;及里面宏任务、微任务的执行过程 事件循环相关描述事件循环流程图案例案例代码案例解析 事件循环相关描述 为什么有事件循环 解决js单线程工作&#xff0c;会堵塞程序的问题 单线程任务类型 同步任务&#xff1a;在主线程上排队&#xff0c;直接执…

南京邮电大学数据库实验一(SQL语言)

文章目录 一、 实验目的和要求二、实验环境(实验设备)三、实验原理及内容1、了解并掌握SQL*Plus环境的使用2、用SQL的DDL语句图书管理系统创建基表3、为基表“读者”补充定义&#xff1a;职称只能取初级、中级、高级之一。4、用SQL的DML语句向上述基表中增加、修改和删除数据5、…