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

您所在的位置:网站首页 hook为什么不能写判断语句 为什么hooks不能在循环、条件或嵌套函数中调用

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

2024-07-18 06:37:51| 来源: 网络整理| 查看: 265

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

handleClick()}> {num} {count}

; } 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( initialState: (() => S) | S, ): [S, Dispatch] { const dispatcher = resolveDispatcher(); return dispatcher.useState(initialState); } function resolveDispatcher() { const dispatcher = ReactCurrentDispatcher.current; return ((dispatcher: any): Dispatcher); } 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; // ……省略 }

从这里可以看出,我们的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( initialState: (() => S) | S, ): [S, Dispatch] { 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 = { pending: null, lanes: NoLanes, dispatch: null, lastRenderedReducer: basicStateReducer, lastRenderedState: (initialState: any), }; hook.queue = queue; // 返回一个dispatch方法用来修改状态,并将此次更新添加update链表中 const dispatch: Dispatch = (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( fiber: Fiber, queue: UpdateQueue, action: A, ): void { const lane = requestUpdateLane(fiber); // 创建更新对象 const update: Update = { 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( queue: UpdateQueue, update: Update, ): 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( initialState: (() => S) | S, ): [S, Dispatch] { return updateReducer(basicStateReducer, (initialState: any)); } updateReducer

updateState 做的事情,实际上就是拿到更新队列,循环队列,并根据每一个 update 对象对当前 hook 进行状态更新,返回最终的结果

function updateReducer( reducer: (S, A) => S, initialArg: I, init?: I => S, ): [S, Dispatch] { 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 | 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 = { 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 = { 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 = (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,最后返回来渲染。

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

为什么?

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



【本文地址】

公司简介

联系我们

今日新闻


点击排行

实验室常用的仪器、试剂和
说到实验室常用到的东西,主要就分为仪器、试剂和耗
不用再找了,全球10大实验
01、赛默飞世尔科技(热电)Thermo Fisher Scientif
三代水柜的量产巅峰T-72坦
作者:寞寒最近,西边闹腾挺大,本来小寞以为忙完这
通风柜跟实验室通风系统有
说到通风柜跟实验室通风,不少人都纠结二者到底是不
集消毒杀菌、烘干收纳为一
厨房是家里细菌较多的地方,潮湿的环境、没有完全密
实验室设备之全钢实验台如
全钢实验台是实验室家具中较为重要的家具之一,很多

推荐新闻


    图片新闻

    实验室药品柜的特性有哪些
    实验室药品柜是实验室家具的重要组成部分之一,主要
    小学科学实验中有哪些教学
    计算机 计算器 一般 打孔器 打气筒 仪器车 显微镜
    实验室各种仪器原理动图讲
    1.紫外分光光谱UV分析原理:吸收紫外光能量,引起分
    高中化学常见仪器及实验装
    1、可加热仪器:2、计量仪器:(1)仪器A的名称:量
    微生物操作主要设备和器具
    今天盘点一下微生物操作主要设备和器具,别嫌我啰嗦
    浅谈通风柜使用基本常识
     众所周知,通风柜功能中最主要的就是排气功能。在

    专题文章

      CopyRight 2018-2019 实验室设备网 版权所有 win10的实时保护怎么永久关闭