一、React Hooks 的底层实现
1.1 核心问题
函数组件每次渲染都会重新执行,本身不保留状态。Hooks 如何「记住」上一次的值?
1.2 本质:链表 + 调用顺序
React 为每个组件实例维护一个 Fiber 节点,Fiber 上有 memoizedState,是一条链表:
1 2 3 4
| useState(0) → Node1 { value: 0 } useState('') → Node2 { value: '' } useEffect(fn) → Node3 { effect: fn } useMemo(calc) → Node4 { value: result }
|
Hooks 按调用顺序依次读取/更新链表上的节点,不做「按名字查找」。
1.2.1 示例:状态如何挂在 Fiber 链表上
组件与 Fiber 一一对应:每个组件实例有一个 Fiber 节点,Fiber 的 memoizedState 指向 Hooks 链表。
以 Counter 组件为例:
1 2 3 4 5 6
| function Counter() { const [count, setCount] = useState(0); const [name, setName] = useState(''); const [flag, setFlag] = useState(false); return <div>{count}</div>; }
|
首次渲染时,React 在 Fiber 上建立如下链表结构:
1 2 3 4 5 6 7 8 9
| Fiber (Counter 组件) └── memoizedState (链表头) │ ▼ Node1: { memoizedState: 0 } ← useState(0) │ next ──────────────────────────────────┐ ▼ Node2: { memoizedState: '' } ← useState('') │ next ──────────────────────────────────┐ ▼ Node3: { memoizedState: false } ← useState(false) next: null
|
简化版实现(核心逻辑):
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24
| const fiber = { type: Counter, memoizedState: null }; let currentHook = null;
function useState(initialValue) { if (!currentHook) { const hook = { memoizedState: initialValue, next: null }; currentHook = hook; } const hook = currentHook; currentHook = hook.next;
const setState = (newValue) => { hook.memoizedState = newValue; scheduleReRender(); }; return [hook.memoizedState, setState]; }
function renderComponent(Component) { currentHook = fiber.memoizedState; return Component(); }
|
两次渲染的流程:
1 2 3 4 5 6 7 8 9 10 11 12 13 14
| 首次渲染: useState(0) → 创建 Node1 { memoizedState: 0 } 返回 [0, setCount] useState('') → 创建 Node2 { memoizedState: '' } 返回 ['', setName] useState(false)→ 创建 Node3 { memoizedState: false } 返回 [false, setFlag]
用户点击,setCount(1): Node1.memoizedState = 1 scheduleReRender()
第二次渲染: currentHook = fiber.memoizedState (Node1) useState(0) → 读 Node1,返回 [1, setCount] ✓ useState('') → 读 Node2,返回 ['', setName] ✓ useState(false)→ 读 Node3,返回 [false, setFlag] ✓
|
| 调用顺序 |
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 2 3 4 5 6 7 8 9 10 11 12 13
| let hooks = []; let currentHookIndex = 0;
function useState(initialValue) { const index = currentHookIndex; const state = hooks[index] !== undefined ? hooks[index] : initialValue; const setState = (newValue) => { hooks[index] = newValue; scheduleReRender(); }; currentHookIndex++; return [state, setState]; }
|
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 2 3 4 5
| let shouldRun = false; if (!deps) shouldRun = true; else if (!prevDeps) shouldRun = true; else shouldRun = deps.some((d, i) => !Object.is(d, prevDeps[i]));
|
示例:
1 2 3 4 5 6 7 8 9 10 11 12 13
| useEffect(() => { console.log('每次渲染都执行'); });
useEffect(() => { console.log('只执行一次'); return () => console.log('卸载时执行'); }, []);
useEffect(() => { console.log('count 或 name 变化时执行'); }, [count, name]);
|
| 情况 |
依赖比较 |
是否执行 |
| 无 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 2 3 4
| const setState = (newValue) => { hooks[index] = newValue; scheduleReRender(); };
|
setState 在首次渲染时创建,通过闭包捕获了当时的 index。即使用户在 3 秒后点击按钮调用 setState,它仍然能正确写入对应节点,因为闭包保留了 index。
2. stale closure(陈旧闭包)问题
事件回调、useEffect 中的函数会闭包捕获当次渲染时的 state。若在回调中直接使用 state,可能拿到旧值:
1 2 3 4 5 6 7 8 9
| function Counter() { const [count, setCount] = useState(0); useEffect(() => { const id = setInterval(() => { setCount(count + 1); }, 1000); return () => clearInterval(id); }, []); }
|
3. 正确做法:函数式更新
1
| setCount(prev => prev + 1);
|
React 会传入最新的 state,避免闭包捕获 stale 值。
4. useCallback / useMemo 与闭包
useCallback(fn, deps) 缓存的是函数本身;若 deps 不变,返回的是同一个函数引用,其闭包捕获的也是旧依赖。useMemo 同理。因此 deps 必须完整列出闭包中用到的所有变量。
1.8 详细分析:一次更新的完整流程
以 useState 为例,从点击按钮到界面更新:
1 2 3 4 5 6 7
| 1. 用户点击 → 触发 onClick → 调用 setState(newValue) 2. setState 通过闭包中的 index 找到链表节点,写入新值 3. 调用 scheduleReRender(),React 将本次更新加入调度队列 4. 调度器在合适时机执行 render 阶段 5. 组件函数重新执行,useState(initialValue) 再次被调用 6. currentHookIndex 从 0 开始,按顺序遍历:hooks[index] 已有新值,返回 [newValue, setState] 7. 组件用新 state 生成新 JSX,进入 commit 阶段,DOM 更新
|
闭包保证了:异步回调中的 setState,在任意时刻被调用时,仍能通过 index 找到正确的链表节点。 链表保证了:多次渲染之间,状态得以保留。 两者缺一不可。
二、Fiber 的核心原理
2.1 一句话
Fiber 把「一次性递归完整个树」的同步更新,拆成「按节点逐步执行、可中断」的增量更新,让渲染不卡顿主线程。
2.2 为什么需要 Fiber?
| 旧版 (Stack Reconciler) |
Fiber 之后 |
| 从根递归整棵树,一气做完 |
拆成一个个小单元 |
| 无法暂停 |
可随时暂停,让出主线程 |
| 大树会长时间占用主线程 |
支持时间分片、优先级调度 |
2.3 Fiber 是什么?
Fiber = 对应一个 React 元素的「工作单元」,携带该节点的类型、props、子节点引用等。
1 2 3 4 5 6 7 8 9
| Fiber { type, key, props child: 第一个子 Fiber sibling: 下一个兄弟 Fiber return: 父 Fiber(用于回溯) alternate: 另一棵树的对应 Fiber(双缓冲) flags: 增/删/改等标记 lane: 优先级 }
|
2.4 链表式遍历(而非递归)
树被转成链表结构,遍历时可随时停下、下次从断点继续:
1 2 3 4 5 6 7
| App / \ Header Main / \ | Nav Logo Content
遍历顺序:App → Header → Nav → Logo → Main → Content ...
|
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 2 3 4 5
| 1. 把大任务拆成小工作单元 2. 每个单元可单独执行、可暂停 3. 用链表/树记录进度,便于恢复 4. 调度器按优先级和时间片决定执行哪些 5. 高优任务可插队
|
四、整体逻辑关系
1 2 3 4 5 6 7 8 9
| Hooks 底层 → 状态存在 Fiber 的链表上,靠调用顺序对应 → 闭包捕获索引,保证 setState 等更新函数能正确写入;需注意 stale closure
Fiber 核心 → 把递归更新改为可中断的链表遍历 + 时间分片
Fiber 思想 → 可中断、可调度、增量工作,具有普适性,可迁移到其他框架(如 Flutter)与场景
|