react的 hooks 是如何存储的
在 React 中,Hooks 的存储与 Fiber 架构紧密相关,其核心机制是通过链表结构将 Hooks 与组件实例关联,并依托 Fiber 节点保存 Hooks 状态。具体实现逻辑如下:
1. Hooks 存储的载体:Fiber 节点
React 中每个组件(函数组件/类组件)都会对应一个 Fiber 节点(虚拟 DOM 的升级版本),用于存储组件的类型、DOM 信息、状态数据等。
对于函数组件,Hooks 的数据就保存在其对应 Fiber 节点的 memoizedState 属性中。
2. Hooks 的存储结构:单向链表
函数组件中定义的多个 Hooks(如 useState、useEffect 等)会以单向链表的形式存储在 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 的存储差异
- 状态类 Hooks(
useState、useReducer):在链表节点中保存具体的状态值(state)和更新队列(queue,用于存储待执行的更新操作)。 - 副作用 Hooks(
useEffect、useLayoutEffect):除了自身状态,还会在链表中保存副作用函数、依赖项数组等,React 会根据依赖项变化决定是否执行副作用。 - 缓存类 Hooks(
useMemo、useCallback):存储计算结果或函数引用,依赖项不变时直接复用缓存值。 - 上下文 Hooks(
useContext):不存储状态,而是通过读取上下文的 Fiber 节点获取最新值。
4. 不同类型 Hooks 的存储差异–详解
要理解不同 React Hooks 的存储差异,核心需先明确 Hooks 的底层存储载体:React 会为每个组件实例维护一条 Hooks 链表,链表的每个节点对应一个 Hook 调用(如一次 useState 或 useEffect)。不同类型 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 为例,其存储与更新的完整链路如下:
-
首次渲染(Mount):
- 组件执行
const [count, setCount] = useState(0)。 - React 为该 Hook 创建链表节点,
memoizedState = 0,并初始化queue(空队列)。 - 将
dispatch函数(即setCount)返回给组件,dispatch内部绑定了当前 Hook 的queue。
- 组件执行
-
触发更新(如调用
setCount(1)):setCount被调用时,会创建一个“更新对象”(包含新值1和更新逻辑),并将其加入queue.pending队列。- React 标记组件为“待更新”,等待批量更新时机(如事件循环结束)。
-
重新渲染(Update):
- 组件重新执行,再次遇到
useState(0)。 - React 按调用顺序找到之前的 Hook 节点,读取
queue中的更新操作,计算出最新状态(1),并更新memoizedState = 1。 - 再次返回
[count: 1, setCount],完成状态更新。
- 组件重新执行,再次遇到
3. useState 与 useReducer 的存储差异
两者核心存储逻辑一致(均依赖 memoizedState 和 queue),差异仅在于 状态更新逻辑的“复杂度”:
useState:memoizedState直接存储“原始状态值”(如数字、字符串、对象),queue中存储的是“简单值更新”(如newState = 1)。useReducer:memoizedState存储“状态对象”(通常更复杂,如{ 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 为例)
useEffect 与 useLayoutEffect 的存储逻辑完全一致,差异仅在于 副作用执行时机(前者在 DOM 渲染后执行,后者在 DOM 渲染前、浏览器绘制前执行),此处以 useEffect 为例:
-
首次渲染(Mount):
- 组件执行
useEffect(() => { fetchData() }, [count])。 - React 创建 Effect 节点,存储
create = () => { fetchData() }、deps = [count](初始count=0),destroy = undefined(首次执行前无清理函数)。 - React 将该 Effect 节点加入组件的“Effect 链表”,等待 DOM 渲染完成后执行。
- 组件执行
-
执行副作用(首次):
- DOM 渲染完成后,React 遍历 Effect 链表,执行当前节点的
create函数(即fetchData())。 - 若
create函数返回清理函数(如() => { cancelFetch() }),则将清理函数存入节点的destroy字段,供后续卸载或更新时使用。
- DOM 渲染完成后,React 遍历 Effect 链表,执行当前节点的
-
重新渲染(Update):
- 组件重新执行,再次遇到
useEffect(...),React 找到对应的 Effect 节点。 - 对比“新依赖项”(当前
count)与“旧依赖项”(节点中存储的deps):- 若依赖项变化:先执行节点中存储的
destroy函数(清理上次副作用),再执行新的create函数,最后更新节点的create和deps。 - 若依赖项不变:直接跳过副作用执行,复用之前的存储信息。
- 若依赖项变化:先执行节点中存储的
- 组件重新执行,再次遇到
-
组件卸载(Unmount):
- React 遍历 Effect 链表,执行每个节点的
destroy函数,清理所有副作用(如取消请求、移除事件监听)。
- React 遍历 Effect 链表,执行每个节点的
三、缓存类 Hooks:useMemo / useCallback —— 存储“计算结果/函数引用与依赖条件”
缓存类 Hooks 用于“避免不必要的计算或函数创建”,优化组件性能。其存储设计的核心是 “依赖不变时复用缓存值”,因此节点中会存储“缓存结果”和“依赖项”。
1. 核心存储内容(链表节点结构)
两者的节点结构类似,核心差异在于 缓存的“值类型”:
| Hook 类型 | 缓存值字段(memoizedValue) | 依赖项字段(deps) | 作用说明 |
|---|---|---|---|
useMemo |
存储“计算结果”(如 sum(1,2) 的结果 3) |
依赖项数组(如 [a, b]) |
依赖不变时,直接返回缓存的计算结果,避免重复执行计算函数。 |
useCallback |
存储“函数引用”(如 () => { handleClick(a) }) |
依赖项数组(如 [a]) |
依赖不变时,直接返回缓存的函数引用,避免每次渲染都创建新函数(导致子组件不必要重渲染)。 |
2. 存储逻辑与工作流程(以 useMemo 为例)
-
首次渲染(Mount):
- 组件执行
const sum = useMemo(() => a + b, [a, b])(假设初始a=1,b=2)。 - React 创建缓存 Hook 节点,执行计算函数
() => a + b,得到结果3,存入memoizedValue = 3,同时存储deps = [1, 2]。 - 将
memoizedValue(即3)返回给组件,作为sum的值。
- 组件执行
-
重新渲染(Update):
- 组件重新执行,再次遇到
useMemo(...),React 找到对应的缓存节点。 - 对比“新依赖项”(当前
a、b)与“旧依赖项”(节点中存储的deps):- 若依赖项变化(如
a=2):重新执行计算函数(2+2=4),更新节点的memoizedValue=4和deps=[2,2],返回新结果。 - 若依赖项不变:直接返回节点中存储的
memoizedValue(3),跳过计算过程。
- 若依赖项变化(如
- 组件重新执行,再次遇到
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 对象”,不存储状态值,具体流程如下:
-
组件关联 Context:
- 组件执行
const theme = useContext(ThemeContext)。 - React 创建
useContext的链表节点,存储context = ThemeContext(即当前关联的 Context 对象)。
- 组件执行
-
读取 Context 值:
- React 从
ThemeContext对应的 Fiber 节点中,读取最新的value(如{ color: 'red' })。 - 将该
value返回给组件,作为theme的值。
- React 从
-
Context 值更新:
- 当
ThemeContext.Provider的value变化时(如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 函数式编程范式的核心实现。
更多推荐
所有评论(0)