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