在 React 中,Hooks 的存储与 Fiber 架构紧密相关,其核心机制是通过链表结构将 Hooks 与组件实例关联,并依托 Fiber 节点保存 Hooks 状态。具体实现逻辑如下:

1. Hooks 存储的载体:Fiber 节点

React 中每个组件(函数组件/类组件)都会对应一个 Fiber 节点(虚拟 DOM 的升级版本),用于存储组件的类型、DOM 信息、状态数据等。
对于函数组件,Hooks 的数据就保存在其对应 Fiber 节点的 memoizedState 属性中。

2. Hooks 的存储结构:单向链表

函数组件中定义的多个 Hooks(如 useStateuseEffect 等)会以单向链表的形式存储在 Fiber 节点的 memoizedState 中:

  • 每个 Hook 自身是一个对象,包含 state(当前状态值)、queue(更新队列)、next(指向下一个 Hook 的指针)等属性。
  • 第一个 Hook 被挂载到 Fiber 节点的 memoizedState 上,后续每个 Hook 通过 next 指针与前一个 Hook 连接,形成链表。

例如,以下组件的 Hooks 存储结构:

function MyComponent() {
  const [name, setName] = useState('');    // Hook 1
  const [age, setAge] = useState(0);       // Hook 2
  useEffect(() => {}, []);                 // Hook 3
}

对应的链表结构:

Fiber.memoizedState → Hook1 → Hook2 → Hook3 → null
(每个 Hook 包含自身状态和 next 指针)

3. Hooks 与组件的关联:调用顺序的重要性

React 依靠Hooks 的调用顺序来匹配链表中的对应节点:

  • 组件首次渲染时,按顺序执行所有 Hooks,依次创建 Hook 对象并串联成链表,挂载到 Fiber 节点。
  • 组件更新时(如状态变化、父组件传参变化),会按完全相同的顺序重新执行 Hooks,通过链表的指针遍历找到对应的 Hook 对象,从而读取/更新状态。

这也是为什么 React 要求Hooks 必须在函数组件顶层调用,不能在条件语句、循环中使用——如果调用顺序改变,会导致链表遍历错位,无法正确匹配状态。

4. 不同类型 Hooks 的存储差异

  • 状态类 HooksuseStateuseReducer):在链表节点中保存具体的状态值(state)和更新队列(queue,用于存储待执行的更新操作)。
  • 副作用 HooksuseEffectuseLayoutEffect):除了自身状态,还会在链表中保存副作用函数、依赖项数组等,React 会根据依赖项变化决定是否执行副作用。
  • 缓存类 HooksuseMemouseCallback):存储计算结果或函数引用,依赖项不变时直接复用缓存值。
  • 上下文 HooksuseContext):不存储状态,而是通过读取上下文的 Fiber 节点获取最新值。

4. 不同类型 Hooks 的存储差异–详解

要理解不同 React Hooks 的存储差异,核心需先明确 Hooks 的底层存储载体:React 会为每个组件实例维护一条 Hooks 链表,链表的每个节点对应一个 Hook 调用(如一次 useStateuseEffect)。不同类型 Hooks 的差异,本质是 链表节点中存储的“核心数据”不同,以及这些数据在 React 渲染流程中承担的角色不同。

下面结合 React 渲染逻辑,对四类 Hooks 的存储细节、设计目的、工作流程展开详细分析:

一、状态类 Hooks:useState / useReducer —— 存储“可变更的状态与更新逻辑”

状态类 Hooks 是组件“状态管理”的核心,其存储设计需满足 “状态持久化”“批量更新” 两大需求,因此链表节点中会存储与“状态变更”直接相关的核心数据。

1. 核心存储内容(链表节点结构)

每个状态类 Hook 节点(Hook 对象)包含以下关键字段(简化版):

字段名 作用说明
memoizedState 存储当前“最新的状态值”(如 useState(0) 中,初始为 0,更新后为新值)。
queue 存储“待执行的更新操作队列”,是实现批量更新的核心。
next 指向链表下一个 Hook 节点,保证 Hooks 调用顺序与存储顺序一致。

其中,queue(更新队列)的结构更复杂,包含:

  • pending:指向待执行的更新链表(采用“环状链表”设计,高效处理批量更新)。
  • dispatch:即我们调用的更新函数(如 setCount),负责将更新操作加入队列。
2. 存储逻辑与工作流程

useState 为例,其存储与更新的完整链路如下:

  1. 首次渲染(Mount)

    • 组件执行 const [count, setCount] = useState(0)
    • React 为该 Hook 创建链表节点,memoizedState = 0,并初始化 queue(空队列)。
    • dispatch 函数(即 setCount)返回给组件,dispatch 内部绑定了当前 Hook 的 queue
  2. 触发更新(如调用 setCount(1)

    • setCount 被调用时,会创建一个“更新对象”(包含新值 1 和更新逻辑),并将其加入 queue.pending 队列。
    • React 标记组件为“待更新”,等待批量更新时机(如事件循环结束)。
  3. 重新渲染(Update)

    • 组件重新执行,再次遇到 useState(0)
    • React 按调用顺序找到之前的 Hook 节点,读取 queue 中的更新操作,计算出最新状态(1),并更新 memoizedState = 1
    • 再次返回 [count: 1, setCount],完成状态更新。
3. useState 与 useReducer 的存储差异

两者核心存储逻辑一致(均依赖 memoizedStatequeue),差异仅在于 状态更新逻辑的“复杂度”

  • useStatememoizedState 直接存储“原始状态值”(如数字、字符串、对象),queue 中存储的是“简单值更新”(如 newState = 1)。
  • useReducermemoizedState 存储“状态对象”(通常更复杂,如 { count: 0, name: 'xxx' }),queue 中存储的是“action 对象”(如 { type: 'INCREMENT', payload: 1 }),更新逻辑由外部 reducer 函数统一处理。
二、副作用 Hooks:useEffect / useLayoutEffect —— 存储“副作用逻辑与依赖条件”

副作用 Hooks 用于处理组件渲染后的“非渲染逻辑”(如请求、DOM 操作),其存储设计需满足 “依赖变化时执行副作用”“组件卸载时清理副作用” 两大需求,因此节点中会存储“副作用函数”“依赖项”“清理函数”等关键信息。

1. 核心存储内容(链表节点结构)

副作用 Hook 节点(Effect 对象)包含以下核心字段:

字段名 作用说明
create 存储用户定义的“副作用函数”(如 () => { fetchData() })。
destroy 存储副作用函数的“清理函数”(如 () => { cancelFetch() }),由 create 函数返回。
deps 存储依赖项数组(如 [count]),React 通过对比前后 deps 决定是否执行 create
next 指向链表下一个 Effect 节点。
2. 存储逻辑与工作流程(以 useEffect 为例)

useEffectuseLayoutEffect 的存储逻辑完全一致,差异仅在于 副作用执行时机(前者在 DOM 渲染后执行,后者在 DOM 渲染前、浏览器绘制前执行),此处以 useEffect 为例:

  1. 首次渲染(Mount)

    • 组件执行 useEffect(() => { fetchData() }, [count])
    • React 创建 Effect 节点,存储 create = () => { fetchData() }deps = [count](初始 count=0),destroy = undefined(首次执行前无清理函数)。
    • React 将该 Effect 节点加入组件的“Effect 链表”,等待 DOM 渲染完成后执行。
  2. 执行副作用(首次)

    • DOM 渲染完成后,React 遍历 Effect 链表,执行当前节点的 create 函数(即 fetchData())。
    • create 函数返回清理函数(如 () => { cancelFetch() }),则将清理函数存入节点的 destroy 字段,供后续卸载或更新时使用。
  3. 重新渲染(Update)

    • 组件重新执行,再次遇到 useEffect(...),React 找到对应的 Effect 节点。
    • 对比“新依赖项”(当前 count)与“旧依赖项”(节点中存储的 deps):
      • 若依赖项变化:先执行节点中存储的 destroy 函数(清理上次副作用),再执行新的 create 函数,最后更新节点的 createdeps
      • 若依赖项不变:直接跳过副作用执行,复用之前的存储信息。
  4. 组件卸载(Unmount)

    • React 遍历 Effect 链表,执行每个节点的 destroy 函数,清理所有副作用(如取消请求、移除事件监听)。
三、缓存类 Hooks:useMemo / useCallback —— 存储“计算结果/函数引用与依赖条件”

缓存类 Hooks 用于“避免不必要的计算或函数创建”,优化组件性能。其存储设计的核心是 “依赖不变时复用缓存值”,因此节点中会存储“缓存结果”和“依赖项”。

1. 核心存储内容(链表节点结构)

两者的节点结构类似,核心差异在于 缓存的“值类型”

Hook 类型 缓存值字段(memoizedValue) 依赖项字段(deps) 作用说明
useMemo 存储“计算结果”(如 sum(1,2) 的结果 3 依赖项数组(如 [a, b] 依赖不变时,直接返回缓存的计算结果,避免重复执行计算函数。
useCallback 存储“函数引用”(如 () => { handleClick(a) } 依赖项数组(如 [a] 依赖不变时,直接返回缓存的函数引用,避免每次渲染都创建新函数(导致子组件不必要重渲染)。
2. 存储逻辑与工作流程(以 useMemo 为例)
  1. 首次渲染(Mount)

    • 组件执行 const sum = useMemo(() => a + b, [a, b])(假设初始 a=1b=2)。
    • React 创建缓存 Hook 节点,执行计算函数 () => a + b,得到结果 3,存入 memoizedValue = 3,同时存储 deps = [1, 2]
    • memoizedValue(即 3)返回给组件,作为 sum 的值。
  2. 重新渲染(Update)

    • 组件重新执行,再次遇到 useMemo(...),React 找到对应的缓存节点。
    • 对比“新依赖项”(当前 ab)与“旧依赖项”(节点中存储的 deps):
      • 若依赖项变化(如 a=2):重新执行计算函数(2+2=4),更新节点的 memoizedValue=4deps=[2,2],返回新结果。
      • 若依赖项不变:直接返回节点中存储的 memoizedValue3),跳过计算过程。
3. useMemo 与 useCallback 的核心差异
  • useMemo:缓存“值”(计算结果),解决“重复计算”问题(如复杂的列表过滤、数据转换)。
  • useCallback:缓存“函数引用”,解决“子组件因父组件函数重新创建而不必要重渲染”的问题(需配合子组件的 React.memo 使用)。
四、上下文 Hooks:useContext —— 不存储状态,仅“读取上下文”

useContext 是唯一“不存储状态”的 Hooks,其核心作用是 “跨组件读取上下文(Context)的值”,无需在自身链表节点中存储状态,而是直接从 Context 对应的 Fiber 节点中获取最新值。

1. 为何不存储状态?

Context 的设计理念是“全局状态共享”:Context 对象本身会维护一个“当前值”(value),并将该值挂载到 React 内部的 Context Fiber 节点 上(而非 useContext 的链表节点)。当 Context 的 value 变化时,所有使用 useContext 读取该 Context 的组件都会被标记为“待更新”。

2. 存储逻辑与工作流程

useContext 的链表节点仅存储“当前关联的 Context 对象”,不存储状态值,具体流程如下:

  1. 组件关联 Context

    • 组件执行 const theme = useContext(ThemeContext)
    • React 创建 useContext 的链表节点,存储 context = ThemeContext(即当前关联的 Context 对象)。
  2. 读取 Context 值

    • React 从 ThemeContext 对应的 Fiber 节点中,读取最新的 value(如 { color: 'red' })。
    • 将该 value 返回给组件,作为 theme 的值。
  3. Context 值更新

    • ThemeContext.Providervalue 变化时(如 value={{ color: 'blue' }}),React 会标记所有使用 useContext(ThemeContext) 的组件为“待更新”。
    • 这些组件重新渲染时,useContext 会再次从 Context 的 Fiber 节点中读取最新 value,完成更新。
五、四类 Hooks 存储差异总结

为了更清晰地对比,我们将四类 Hooks 的核心存储字段、设计目的、状态来源整理如下表:

Hooks 类型 核心存储字段 设计目的 状态/值来源
状态类(useState/useReducer) memoizedState(状态值)、queue(更新队列) 组件状态管理,支持批量更新 组件内部,由更新函数(setCount/dispatch)修改
副作用类(useEffect/useLayoutEffect) create(副作用函数)、destroy(清理函数)、deps(依赖项) 处理渲染后副作用,依赖变化时执行 组件内部定义,依赖外部变量(如 props/state)
缓存类(useMemo/useCallback) memoizedValue(缓存值/函数)、deps(依赖项) 复用计算结果/函数引用,优化性能 计算函数执行结果或函数引用,依赖外部变量
上下文类(useContext) context(关联的 Context 对象) 跨组件读取 Context 值,实现状态共享 Context.Provider 的 value,外部注入

通过以上分析可见,React Hooks 的存储设计完全围绕其“功能定位”展开:状态类 Hooks 聚焦“状态变更”,副作用类 Hooks 聚焦“逻辑执行条件”,缓存类 Hooks 聚焦“结果复用”,上下文类 Hooks 聚焦“跨组件取值”—— 所有设计最终都是为了高效维护组件状态与逻辑,优化渲染性能

总结

Hooks 的存储本质是:以单向链表形式将 Hooks 状态挂载到组件对应的 Fiber 节点上,通过固定的调用顺序实现状态的读取与更新。这种设计既保证了函数组件能拥有状态管理能力,又避免了类组件的 this 指向问题,是 React 函数式编程范式的核心实现。

Logo

腾讯云面向开发者汇聚海量精品云计算使用和开发经验,营造开放的云计算技术生态圈。

更多推荐