聊聊 Hook 如何在函数组件的多次渲染之间保存状态、处理副作用
先记住一句话:Hook 依赖稳定的调用顺序,把状态和副作用信息挂载到组件对应的 Fiber 上;函数组件每次重新执行时,React 按相同顺序取回这些信息。
本文介绍 Hook 的整体模型,具体 API 可以继续阅读:
React 要求 Hook 只能在函数组件或自定义 Hook 的顶层调用,不能放在条件、循环、嵌套函数或事件处理函数中:
React 不依赖 Hook 的变量名来识别它,而是依赖调用顺序。可以把一个组件简化理解成这样的 Hook 链表:
首次渲染时,React 按顺序创建 Hook 信息;后续渲染时,React 仍按这个顺序读取。上面的例子在 show 从 true 变为 false 后,后续 Hook 的位置就会发生偏移,React 无法再把每次调用准确对应到原来的状态。
因此,Hook 规则不是语法限制,而是 React 能够在没有组件实例的情况下管理状态的前提。
函数组件本身只是一次函数调用,不负责持有跨渲染状态。React 会为组件维护 Fiber 节点,Fiber 上保存了与该组件有关的渲染信息。
在简化模型中:
memoizedState(注意拼写,不是 memorizedState)指向 Hook 链表。不同 Hook 节点会保存不同信息,例如:
useState / useReducer:当前状态和更新队列useRef:稳定的 ref 对象useMemo / useCallback:缓存值和依赖useEffect:effect、依赖和清理信息这是帮助理解的简化模型。真实 React 还会通过 Fiber 的 flags、提交阶段等机制处理副作用,并使用 current 与 workInProgress Fiber 进行更新。
useState 如何保存状态?大致流程如下:
setCount 被调用时,React 创建一个更新对象并放入更新队列。更新不会直接修改当前渲染中的 count。因此多次更新时,推荐使用函数式写法:
直接使用闭包中的旧值可能得到不同结果:
React 会使用 Object.is 判断状态是否发生变化。更新对象或数组时,应返回新的引用,而不是原地修改旧对象。
useEffect 如何处理副作用?渲染阶段应该保持纯粹:根据 props 和 state 计算 UI。订阅事件、请求数据、操作 DOM、启动定时器等副作用,应放在 effect 中。
useEffect 的回调在组件提交更新后由 React 调度执行,通常会让浏览器先完成绘制;它不应被理解为永远严格发生在绘制之后。需要在绘制前读取或调整布局时,才考虑 useLayoutEffect。
React 会逐项使用 Object.is 比较新旧依赖值:
useEffect(fn, []):不因后续重新渲染重新执行;组件卸载时执行清理。开发环境 Strict Mode 可能额外执行一次 setup/cleanup 检查。useEffect(fn, [count]):首次提交后执行,之后在 count 发生变化时重新执行。useEffect(fn):每次提交后都可能执行。清理函数会在 effect 重新执行前运行,也会在组件卸载时运行:
每次渲染都是一次独立的函数调用,effect 捕获的是创建它的那次渲染中的变量:
解决方式通常是:
useRef,并明确管理同步逻辑。依赖数组不是“控制 effect 执行次数”的开关,而是 effect 对外部响应式值的声明。应优先让依赖保持诚实,并通过重构逻辑解决不必要的重复执行。
| Hook | 主要用途 |
|---|---|
useState | 保存组件局部状态 |
useReducer | 用 reducer 管理更复杂的状态更新 |
useRef | 保存跨渲染稳定的可变值,修改它不会触发渲染 |
useMemo | 缓存计算结果,不是通用的状态存储方案 |
useCallback | 缓存函数引用,常用于配合 memo 或稳定依赖 |
useContext | 读取上下文值 |
| 自定义 Hook | 复用带状态的逻辑,每次调用仍拥有独立状态 |
这些 Hook 仍然遵守同一条规则:调用顺序必须稳定。
React 18 会在更多场景下批量处理状态更新,减少不必要的重新渲染。批处理不改变 Hook 的调用规则,也不意味着 setState 会立即修改当前渲染中的变量。
开发环境 Strict Mode 可能主动执行额外的渲染和 effect setup/cleanup 检查,用来发现不纯的渲染逻辑和缺少清理的副作用。因此 effect 必须能够正确清理并安全地重复执行。
现代 React 的渲染阶段可能被中断、重做或放弃。不要在渲染阶段执行有副作用的操作;副作用应放在 effect 或事件处理函数中。
可以这样概括:
Hook 不是依靠函数变量保存状态,而是依靠稳定的调用顺序,让 React 在组件对应的 Fiber 上找到相应的 Hook 信息。
useState通过更新队列计算新状态,useEffect在提交阶段处理副作用。调用顺序一旦改变,Hook 与状态的对应关系就会错位。
继续展开时,可以补充:
Object.is 比较,不是比较数组本身的引用。useMemo、useCallback 是性能优化工具,不是保证永久缓存的语义工具。