react - Fiber架构初步理解

📅 发布时间:2026/8/12 21:28:09
react - Fiber架构初步理解 文章目录一、引言useState 的状态丢失谜题1.1 闭包Closure的作用与陷阱⚠️ 闭包陷阱与批处理机制1.2 Fiber 节点的状态存储机制二、React Fiber 架构深度剖析2.1 Fiber 节点的核心数据结构2.2 Fiber 节点的组织形式真正的链表树2.3 Hooks 在 Fiber 中的单向链表排布2.4 为什么 React 选用链表而不是数组2.5 双缓存机制Double Buffering2.6 Fiber 树的 DFS 遍历过程与伪代码三、链表数据结构基础与 JS 实现3.1 什么是单链表3.2 单链表的 JavaScript 实现3.3 链表 vs 数组 性能对比四、总结一、引言useState 的状态丢失谜题在 React 函数组件中每次组件重新渲染组件函数都会从头到尾重新执行一次。但为什么通过useState定义的状态State却能在多次渲染之间持久保存更新后的值而不会被重复初始化呢其背后的底层机制涉及闭包Closure与React Fiber 架构的紧密协作。1.1 闭包Closure的作用与陷阱useState返回的状态值如count和更新函数如setCount通过闭包捕获了当前渲染周期的状态快照。function Counter() { const [count, setCount] useState(0); // 闭包捕获当前渲染周期的 count 值 const handleClick () { // 提交更新闭包中的 count 是当前渲染周期的值例如 0 setCount(count 1); }; return button onClick{handleClick}{count}/button; }⚠️ 闭包陷阱与批处理机制看下面这个常见场景function Counter() { const [count, setCount] useState(0); const handleClick () { setCount(count 1); // 第一次更新基于当前闭包 count 0 setCount(count 1); // 第二次更新仍基于当前闭包 count 0 }; // 点击后 count 只会变为 1而不是 2 return button onClick{handleClick}{count}/button; }原因一闭包在同一次事件回调中两次count拿到的都是旧快照0。原因二批处理与计算传入的是具体数值setCount(1)React 在下一轮合并更新时会将旧值覆盖。解决方案如果需要依赖最新状态需改用函数式更新setCount(prev prev 1)React 会将更新函数放入队列依次计算出最终状态。1.2 Fiber 节点的状态存储机制闭包负责在单次渲染中提供状态快照而真正跨渲染周期存储状态的地方是 React 的 Fiber 节点。每个函数组件在 React 内部都对应一个 Fiber 节点其状态链表存储在 Fiber 节点的memoizedState属性中。组件重新渲染时useState不会重新初始化状态而是直接从 Fiber 节点的memoizedState中读取最新计算好的状态值。// Fiber 节点存储 Hook 状态的结构示意{memoizedState:{memoizedState:42,// 当前 Hook 的状态值queue:{...},// 待处理的 setState 更新队列next:{...}// 指向下一个 Hook}}二、React Fiber 架构深度剖析在 React 16 之前Stack Reconciler渲染过程通过原生的递归函数调用栈完成一旦开始就无法中断容易导致主线程卡顿。引入Fiber 架构后React 将树形结构改造成了指针链表从而实现了可中断、可恢复的增量渲染。2.1 Fiber 节点的核心数据结构每一个 Fiber 节点都是一个 JavaScript 对象包含了组件类型、状态、DOM 节点以及调度所需的链表指针interfaceFiber{// 1. 标识组件/节点类型tag:WorkTag;// 如 FunctionComponent, ClassComponent, HostComponenttype:any;// 对于函数组件type 是组件函数本身对于 DOM 节点是 div 等字符串// 2. 状态与副作用存储memoizedState:any;// 状态存储函数组件的 Hooks 单向链表保存在这里stateNode:any;// 对应的真实 DOM 节点或类组件实例updateQueue:UpdateQueueany|null;// 状态更新队列flags:Flags;// 副作用标记如 Placement 插入、Update 更新、Deletion 删除// 3. 核心指针链表构成 Fiber 树的关键child:Fiber|null;// 指向“第一个子节点”sibling:Fiber|null;// 指向“下一个兄弟节点”return:Fiber|null;// 指向“父节点”// 4. 双缓存机制指针alternate:Fiber|null;// 指向另一棵树current - workInProgress对应的 Fiber 节点}2.2 Fiber 节点的组织形式真正的链表树以如下组件结构为例function App() { return ( div Header / Content / /div ); }传统树形结构是用数组包含子节点而Fiber 树本质上是一棵通过指针串联起来的单链表架构App Fiber | (child) div Fiber | (child) Header Fiber ----(sibling)---- Content Fiber | (return) | (return) ----------------------------------- div Fiber重点父节点的child仅仅指向第一个子节点Header。子节点通过sibling链表指向下一个兄弟节点Content。所有子节点通过return指向其父节点div。2.3 Hooks 在 Fiber 中的单向链表排布函数组件内的多个 Hook如useState,useEffect按调用顺序存在于 Fiber 的memoizedState中以单向链表的形式组织function Example() { const [count, setCount] useState(0); // Hook 1 const [name, setName] useState(); // Hook 2 useEffect(() {}, []); // Hook 3 }对应的 Hook 内存链表Fiber.memoizedState | Hook1 (count) ----next---- Hook2 (name) ----next---- Hook3 (effect) ---- null | | | memoizedState: 0 memoizedState: memoizedState: { ... }为什么 React 规定 Hook 不能写在条件语句或循环中因为 React 内部依靠单向链表的游标指针按 Hook 执行顺序逐个读取节点。如果条件分支导致某个 Hook 被跳过链表结点的匹配就会全部错位2.4 为什么 React 选用链表而不是数组可中断与恢复最核心原因原生的递归调用栈无法暂停。而使用链表指针模拟调用栈后React 可以将当前工作节点记录在一个全局指针如workInProgress中。当浏览器帧率不足通过Scheduler调度时React 随时可以中断Pause渲染让出主线程下一帧再从记录的指针处恢复Resume遍历。高效的结构变更在树的 DOM 节点插入、删除、移动时链表只需要修改指针如child或sibling操作的时间复杂度为O ( 1 ) O(1)O(1)效率极高。保证 Hook 调用的顺序确定性单向链表天生具备严格的前后依赖关系能以极低的开销保障 Hook 调用的固定顺序。2.5 双缓存机制Double Buffering在 2.1 节提到了alternate属性这是 React 优化 DOM 渲染的核心思想——双缓存机制current树代表当前在屏幕上显示的真实 DOM 对应的 Fiber 树。workInProgress树代表正在内存中构建/更新的 Fiber 树。当触发更新时React 会在后台基于current树构建全新的workInProgress树。所有 Diff 比较与副作用标记都在后台完成后React 只需要将指针切换一次直接把workInProgress替换为current即可无缝完成界面更新避免中间状态导致的页面闪烁。2.6 Fiber 树的 DFS 遍历过程与伪代码React 遍历 Fiber 树采用的是深度优先遍历DFS整个过程被称为WorkLoop工作循环// 模拟 Fiber 树单次单元工作的伪代码functionperformUnitOfWork(fiber){// 1. 执行当前 Fiber 节点的 reconcile/更新逻辑 ...// 2. 如果有子节点优先深入向下遍历 childif(fiber.child){returnfiber.child;}// 3. 如果没有子节点说明到了叶子节点查找兄弟节点或向父节点回溯letnextFiberfiber;while(nextFiber){// 完成当前节点的“归complete Work”逻辑 ...// 如果有兄弟节点走 sibling 指针if(nextFiber.sibling){returnnextFiber.sibling;}// 否则回溯给父节点 return 指针继续向上查找nextFibernextFiber.return;}returnnull;// 回溯到 Root 节点遍历结束}三、链表数据结构基础与 JS 实现为了更好地理解 Fiber 的设计我们回顾一下数据结构中的单链表原理。3.1 什么是单链表链表Linked List是一种线性数据结构由一系列节点Node组成。节点在内存中无需连续存储每个节点包含数据域存储节点的值/状态指针域指向下一个节点的引用3.2 单链表的 JavaScript 实现// 1. 定义链表节点结构classListNodeT{value:T;next:ListNodeT|null;constructor(value:T){this.valuevalue;this.nextnull;}}// 2. 创建单链表: 1 - 2 - 3constnode1newListNode(1);constnode2newListNode(2);constnode3newListNode(3);node1.nextnode2;node2.nextnode3;// 3. 遍历单链表letcurrent:ListNodenumber|nullnode1;while(current!null){console.log(current.value);// 依次输出: 1, 2, 3currentcurrent.next;}3.3 链表 vs 数组 性能对比特性数组 (Array)链表 (Linked List)内存分配连续内存空间动态随机分配内存非连续随机访问 (Index)快时间复杂度O ( 1 ) O(1)O(1)慢需要沿指针遍历时间复杂度O ( n ) O(n)O(n)头部/中间插入与删除慢需要移动后续所有元素O ( n ) O(n)O(n)快只需改变对应指针指向O ( 1 ) O(1)O(1)适用场景需要快速检索/遍历元素的场景元素频繁变动、大小不固定、需要随时中断/中断恢复的场景四、总结React 将复杂的 UI 界面更新拆解为高效率的计算调度其核心贯穿逻辑如下useState的存储依赖组件对应的Fiber 节点其内部以单向链表的形式在memoizedState中保存每个 Hook 的状态。闭包与快照组件函数每次渲染通过闭包获取当次渲染的状态快照修改状态时使用函数式更新可以避免闭包拿旧值的问题。Fiber 的链表拓扑Fiber 节点通过child子、sibling兄、return父三条指针将组件树串联为链表结构。可中断渲染的基石正是基于这种显示的指针链表结构React 才能脱离原生 JS 递归栈的限制实现无卡顿的增量渲染Incremental Rendering与双缓存机制。 感谢阅读想了解更多 我的博客网站 | 记录思考分享干货 我的个人主页 | 关于我、开源项目