React Hooks 与 Fiber:原理与应用

一、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); // Hook 1
const [name, setName] = useState(''); // Hook 2
const [flag, setFlag] = useState(false); // Hook 3
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 };
// 挂到 fiber.memoizedState 链表...
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; // 无 deps:每次都执行
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
// 1. 无 deps:每次渲染后执行
useEffect(() => { console.log('每次渲染都执行'); });

// 2. 空 deps:仅首次
useEffect(() => {
console.log('只执行一次');
return () => console.log('卸载时执行');
}, []);

// 3. 有 deps:依赖变化时执行
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 的实现本质可归纳为三点:

  1. 外部存储:状态不存于组件函数内部,而是存在 Fiber 的 memoizedState 链表上。组件每次渲染都会重新执行,但链表在组件实例的生命周期内持续存在。

  2. 顺序索引:每次调用 Hook 时,React 通过一个「当前索引」决定读写哪个链表节点。索引随调用递增,因此必须保证每次渲染的调用顺序、数量完全一致。

  3. 调度更新setState 等更新函数会修改链表上的值,并触发 React 的调度器安排一次重渲染。下次渲染时,组件函数重新执行,Hooks 按相同顺序从链表中读出最新值。

三者结合:外部存储解决「函数无状态」问题,顺序索引解决「多个 Hook 如何区分」问题,调度更新解决「变化如何驱动重渲染」问题。

1.7 闭包机制与 Hooks 的关系

Hooks 与闭包紧密相关:

1. setState 依赖闭包捕获索引

1
2
3
4
const setState = (newValue) => {
hooks[index] = newValue; // index 来自闭包
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); // count 被闭包捕获,一直是 0
}, 1000);
return () => clearInterval(id);
}, []);
}

3. 正确做法:函数式更新

1
setCount(prev => prev + 1);  // 传入函数,React 注入最新 state

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)与场景
Author

Felix Tao

Posted on

2021-12-03

Updated on

2022-01-21

Licensed under