一、React Hooks 的底层实现
1.1 核心问题
函数组件每次渲染都会重新执行,本身不保留状态。Hooks 如何「记住」上一次的值?
1.2 本质:链表 + 调用顺序
React 为每个组件实例维护一个 Fiber 节点,Fiber 上有 memoizedState,是一条链表:
1 | useState(0) → Node1 { value: 0 } |
Hooks 按调用顺序依次读取/更新链表上的节点,不做「按名字查找」。
1.2.1 示例:状态如何挂在 Fiber 链表上
组件与 Fiber 一一对应:每个组件实例有一个 Fiber 节点,Fiber 的 memoizedState 指向 Hooks 链表。
以 Counter 组件为例:
1 | function Counter() { |
首次渲染时,React 在 Fiber 上建立如下链表结构:
1 | Fiber (Counter 组件) |
简化版实现(核心逻辑):
1 | const fiber = { type: Counter, memoizedState: null }; |
两次渲染的流程:
1 | 首次渲染: |
| 调用顺序 | Hook | 对应链表节点 | 存的值 |
|---|---|---|---|
| 1 | useState(0) | Node1 | 0 → 1 |
| 2 | useState(‘’) | Node2 | ‘’ |
| 3 | useState(false) | Node3 | false |
结论:状态不在组件函数里,而在该组件对应 Fiber 的 memoizedState 链表上,每个 Hook 按顺序对应链表中的一个节点。
1.3 为什么顺序不能变?
- 第 1 次调用 Hook → 用链表第 1 个节点
- 第 2 次调用 Hook → 用链表第 2 个节点
- 顺序变了,就错位了
因此 Rules of Hooks 要求:顶层调用、不在循环/条件中。
1.4 各 Hook 的简化实现
useState:
1 | let hooks = []; |
useEffect: 将 callback 和 deps 存入链表,在 commit 阶段执行;依赖变化时重新执行。
useCallback / useMemo: 缓存上一次的值/函数,依赖数组变化才重新计算/创建。
1.4.1 useEffect 的三种情况
| 写法 | 何时执行 | 常见用途 |
|---|---|---|
useEffect(fn) |
每次渲染后 | 很少用 |
useEffect(fn, []) |
仅首次挂载后 | 订阅、初始化、只跑一次 |
useEffect(fn, [a, b]) |
a 或 b 变化时 | 依赖变化时执行副作用 |
实现思路:React 在链表节点上存「上次的 callback」和「上次的 deps」,每次渲染时比较 deps(用 Object.is):
- 无 deps → 每次都执行
- 空数组
[]→ 首次执行(prevDeps 为空) - 有 deps → 逐项比较,任一变则执行
1 | // 简化实现 |
示例:
1 | // 1. 无 deps:每次渲染后执行 |
| 情况 | 依赖比较 | 是否执行 |
|---|---|---|
| 无 deps | 不比较 | 每次 |
[] |
首次 prevDeps 为空 | 只执行一次 |
[a,b] |
用 Object.is 逐项比较 | a 或 b 变化时才执行 |
1.5 小结
| 概念 | 本质 |
|---|---|
| 状态存储 | 存在 Fiber 上的链表,不在函数内部 |
| Hook 识别 | 靠调用顺序对应到链表节点 |
| Rules of Hooks | 保证顺序和数量恒定,才能一一对应 |
1.6 Hooks 的实现本质
Hooks 的实现本质可归纳为三点:
外部存储:状态不存于组件函数内部,而是存在 Fiber 的
memoizedState链表上。组件每次渲染都会重新执行,但链表在组件实例的生命周期内持续存在。顺序索引:每次调用 Hook 时,React 通过一个「当前索引」决定读写哪个链表节点。索引随调用递增,因此必须保证每次渲染的调用顺序、数量完全一致。
调度更新:
setState等更新函数会修改链表上的值,并触发 React 的调度器安排一次重渲染。下次渲染时,组件函数重新执行,Hooks 按相同顺序从链表中读出最新值。
三者结合:外部存储解决「函数无状态」问题,顺序索引解决「多个 Hook 如何区分」问题,调度更新解决「变化如何驱动重渲染」问题。
1.7 闭包机制与 Hooks 的关系
Hooks 与闭包紧密相关:
1. setState 依赖闭包捕获索引
1 | const setState = (newValue) => { |
setState 在首次渲染时创建,通过闭包捕获了当时的 index。即使用户在 3 秒后点击按钮调用 setState,它仍然能正确写入对应节点,因为闭包保留了 index。
2. stale closure(陈旧闭包)问题
事件回调、useEffect 中的函数会闭包捕获当次渲染时的 state。若在回调中直接使用 state,可能拿到旧值:
1 | function Counter() { |
3. 正确做法:函数式更新
1 | setCount(prev => prev + 1); // 传入函数,React 注入最新 state |
React 会传入最新的 state,避免闭包捕获 stale 值。
4. useCallback / useMemo 与闭包
useCallback(fn, deps) 缓存的是函数本身;若 deps 不变,返回的是同一个函数引用,其闭包捕获的也是旧依赖。useMemo 同理。因此 deps 必须完整列出闭包中用到的所有变量。
1.8 详细分析:一次更新的完整流程
以 useState 为例,从点击按钮到界面更新:
1 | 1. 用户点击 → 触发 onClick → 调用 setState(newValue) |
闭包保证了:异步回调中的 setState,在任意时刻被调用时,仍能通过 index 找到正确的链表节点。 链表保证了:多次渲染之间,状态得以保留。 两者缺一不可。
二、Fiber 的核心原理
2.1 一句话
Fiber 把「一次性递归完整个树」的同步更新,拆成「按节点逐步执行、可中断」的增量更新,让渲染不卡顿主线程。
2.2 为什么需要 Fiber?
| 旧版 (Stack Reconciler) | Fiber 之后 |
|---|---|
| 从根递归整棵树,一气做完 | 拆成一个个小单元 |
| 无法暂停 | 可随时暂停,让出主线程 |
| 大树会长时间占用主线程 | 支持时间分片、优先级调度 |
2.3 Fiber 是什么?
Fiber = 对应一个 React 元素的「工作单元」,携带该节点的类型、props、子节点引用等。
1 | Fiber { |
2.4 链表式遍历(而非递归)
树被转成链表结构,遍历时可随时停下、下次从断点继续:
1 | App |
2.5 双缓冲 (alternate)
- current:当前屏幕上的树
- workInProgress:正在构建的新树
更新时在 workInProgress 上增量构建,commit 阶段一次性切换,避免闪烁。
2.6 总结
| 问题 | 回答 |
|---|---|
| Fiber 是什么? | 一个工作单元,带 child/sibling/return 的链表节点 |
| 为什么用? | 实现可中断、可恢复、可调度 |
| 如何可中断? | 链表遍历 + 时间分片 |
| 双缓冲? | 两棵树交替,保证更新可复用且一次性提交 |
三、Fiber 作为一种设计思想
3.1 Fiber 可抽象为通用思想
| 思想 | 含义 |
|---|---|
| 可中断的增量工作 | 大任务拆成小单元,做一点可停 |
| 时间分片 | 固定时间片内工作,到点让出主线程 |
| 优先级调度 | 高优任务(如用户输入)插队 |
| 链表式遍历 | 用 child/sibling/return 实现可暂停、可恢复 |
这些思想可迁移到 UI 渲染、动画、大数据处理等场景。
3.2 Flutter 能否引入?
可以。Flutter 已具备类似机制:
| Flutter 机制 | 对应思想 |
|---|---|
| SchedulerBinding | 分阶段调度(microtask、frame、idle) |
| 优先级队列 | 高优任务优先执行 |
| Isolate | 重计算放到后台,减轻 UI 线程压力 |
若要在 layout 等环节做「增量可中断」,可借鉴 Fiber 的拆解与调度方式。
3.3 通用模式
1 | 1. 把大任务拆成小工作单元 |
四、整体逻辑关系
1 | Hooks 底层 |