news 2026/9/18 4:03:20

React FiberRoot源码解析:核心属性与调度机制详解

作者头像

张小明

前端开发工程师

1.2k 24
文章封面图
React FiberRoot源码解析:核心属性与调度机制详解

1. FiberRoot 到底在 React 生态里扮演什么角色

打开 React 源码,在react-reconciler包里翻ReactFiberRoot.js,第一眼看到FiberRootNode这个构造函数时,你可能会有点懵。它上面挂了一批属性:tagcontainerInfocurrentpendingChildrenexpirationTimescallbackNode…… 每个单独看都能理解,但串起来就不知道它们各自在什么阶段被读、被写、被清空。这篇文章就从源码角度把这堆属性一个个扒开,讲清楚它们分别在创建、更新、提交的哪个环节起作用,以及在排错和面试中怎么用。

先说结论:FiberRoot 是“整个 React 应用的根”,但它不是 Fiber 树上的节点,它是一个独立对象,专门用来保存与整个应用实例相关的全局状态。与之容易混淆的是 RootFiber,也就是 HostRoot 类型的 Fiber 节点,它们之间的关系是一个双向引用。React 所有更新任务最终都经由 FiberRoot 来统一调度和协调,包括当前渲染的树、过期任务、挂载的真实容器、Future 形态的并发更新信息,全部都收口在这个对象上。

我第一次看这段源码时踩过的坑是:以为current指向的是页面根组件对应的 Fiber。后来仔细跟了createFiberRoot的调用链才发现,它指向的是那个tag === 3的 HostRoot Fiber,这个 Fiber 是整个 Fiber 树的起点,并不是 App 本身。理解了这个区别,后面看updateContainerperformConcurrentWorkOnRoot的调度逻辑就顺了很多。

这也就解释了为什么源码里会同时存在两个“根”概念:FiberRoot 是应用级管理者,RootFiber 是 fiber 树级起点。很多源码解读文章会把这两个混着说,结果读源码时经常对不上号。这篇文章所有涉及“FiberRoot 节点”的描述,指的都是FiberRootNode实例本身,涉及current指向的对象时,我会单独说“RootFiber”。

在实际运行时,FiberRoot 承载的信息包括但不限于:当前 Fiber 树的指针、正在构造中的新树、尚未处理的更新队列、调度器回调的状态、过期更新的计时、以及 hydration 相关标记。这些属性分布在 React 17 的FiberRootNode上,到 React 18 有些被 Lane 模型替代,但整体设计思路没有变化。

为了方便对照,下面在讲属性之前,先给出一张我自己维护的速查表,后面逐个展开时就不用反复翻源码。

属性名作用阶段一句话职责
tag创建时标记 Root 类型(LegacyRoot / ConcurrentRoot)
containerInfo创建、提交指向真实 DOM 容器或其他宿主环境容器
current创建、更新、提交指向当前已提交的 Fiber 树根节点
pendingChildren提交初期暂存下一次要提交的 Fiber 树,供致谢生命周期读取
pingCache更新调度缓存被挂起的 Promise 及其回调集合
expirationTimes更新调度(17)记录每个 Lane 对应的到期时间
callbackNode更新调度存储当前调度器的回调节点,用于取消任务
callbackPriority更新调度当前调度任务的优先级
nextKnownPendingExpirationTime(17)更新调度记录最早过期的挂起更新时间,用于渲染优先级判断
memoizedState(17)DOM 操作指向当前页面的可视化快照,用于getPublicRootInstance
suspendedLanes(18)更新调度记录被挂起的 Lane 集合,用于重试策略
finishedWork提交阶段指向已渲染完毕但尚未提交给 DOM 的 Fiber 树

这个表格不是背下来的,是我在调试 React 17 某个过期更新问题时,一行一行打断点总结出来的。后面每个属性都会给出源码位置、赋值时机和读取场景,看完你也能自己总结出一份。

2. 从 createFiberRoot 开始,理解 FiberRoot 是怎么诞生的

2.1 root 参数的来源和完整初始化流程

FiberRootNode的实例不是直接new出来的,它是通过createFiberRoot创建的,而这个函数的参数containerInfo是由宿主环境传进来的。在浏览器中就是document.getElementById('root')这样的真实 DOM 元素,在 React Native 中则是 native 侧的容器对象。

createFiberRoot的源码逻辑非常紧凑,一共做了三件事:new 一个 FiberRootNode,创建对应的 RootFiber,然后通过root.currentfiber.stateNode把这两者互相挂上。这里有一个细节值得注意:createHostRootFiber会根据tag来决定是创建 LegacyRoot 还是 ConcurrentRoot,两者的 Fiber 节点在mode上就有差异,后续整个树的并发行为都从这里分叉。

// 伪代码,位置:packages/react-reconciler/src/ReactFiberRoot.js export function createFiberRoot( containerInfo: any, tag: RootTag, hydrate: boolean, hydrationCallbacks: null | SuspenseHydrationCallbacks, ): FiberRoot { const root: FiberRoot = (new FiberRootNode(containerInfo, tag, hydrate): any); const uninitializedFiber = createHostRootFiber(tag); root.current = uninitializedFiber; uninitializedFiber.stateNode = root; initializeUpdateQueue(uninitializedFiber); return root; }

initializeUpdateQueue这一步很容易被忽略,但它极其关键。它给uninitializedFiber.updateQueue创建了一个空链表,并用shared.pending作为链表的尾指针。既然 HostRoot 是一个 fiber 节点,自然要遵循 fiber 的更新机制,任何从根部发起的更新(比如ReactDOM.render)都要先进入这个updateQueue才能被调度。

从 createFiberRoot 返回之后,ReactDOM 还会调用markContainerAsRoot,把 FiberRoot 挂到真实 DOM 节点的一个隐藏属性上,具体是root.__reactContainer$xxx。这就是为什么你可以在 DevTools 里通过 DOM 元素找到它对应的 React 根实例,断点调试时非常有用。

2.2 FiberRoot 与 RootFiber 双向绑定的必要性与误区

很多新手看这段源码时都会问:为什么root.current不直接指向 App 组件对应的 fiber?这是因为 HostRoot 一个节点承担了整棵树的起点和挂载点两个职责。它一方面把当前树的顶层 fiber 串起来,另一方面持有对 FiberRoot 的反向引用,之后任何从根部发起的更新都要经由root.current.updateQueue进入任务队列。这个设计让 React 在协调两个“根”的关系时只需要处理一对引用:root.currentfiber.stateNode

fiber.stateNode这个属性在不同类型的 fiber 上有不同的含义:函数组件的 fiber 上它指向组件实例,DOM 节点的 fiber 上它指向对应的真实 DOM。HostRoot 的 fiber 则将stateNode指向 FiberRoot 对象,这也是为什么你可以从任意一个 fiber 沿着return链走到最顶端,再从stateNode拿到全局 root。

这里有一个常见的误区:把 FiberRoot 当成了 Fiber 树的一个节点。实际上 FiberRoot 和 RootFiber 是两个独立对象,FiberRoot 管的是“应用的全局状态”,而 RootFiber 是“fiber 树的第一站”。两者之间currentstateNode双向绑定,且这个绑定关系在应用生命周期内是恒定的,不会因为更新而被替换。

提示:折叠起来说,面试中如果被问“React 的根是什么”,可以先说结论“FiberRoot 是应用实例的根,RootFiber 是 fiber 树的根”,再用root.currentfiber.stateNode来说清楚二者的关系。

3. 逐个拆解 FiberRoot 上那些重要属性

3.1 tag 属性——三种 Root 类型与 React 版本演进的秘密

tagFiberRootNode构造函数里是最靠前赋值的属性之一,它标记了当前 React 应用的根类型。React 17 里最常见的两个取值是LegacyRootConcurrentRoot,前者对应的就是ReactDOM.render,后者对应ReactDOM.createRoot。React 的源码里,RootTag类型定义在ReactRootTags.js中:

export type RootTag = 0 | 1 | 2; export const LegacyRoot = 0; export const ConcurrentRoot = 1;

这个值直接决定了createHostRootFiber里面给 fiber 设置的mode。如果是 LegacyRoot,mode 就是NoMode | BlockingMode之类的组合,不会开启并发特性;如果是 ConcurrentRoot,mode 会带上ConcurrentMode,后续的 Lane 调度、可中断渲染、Suspense 这些能力才可用。

tag这个属性本身在运行期几乎不会被修改,但排错时非常有用。比如你在调试一个用ReactDOM.render启动的旧项目,发现调度行为非常“同步”,此时去看 FiberRoot 的tag,就知道它到底是不是兼容模式的根。React 18 中新增了对 hydrate root 的细分,但核心思路没有变。

3.2 containerInfo——从浏览器 DOM 到 React Native 宿主对象的统一抽象

containerInfo的字面意思就是“容器的信息”。在浏览器环境里,它一般是真实的 DOM 节点,比如document.getElementById('root')。但在 React Native 中,它代表的则是一个由宿主环境提供的“根视图”对象,并不具备 DOM 属性。这个属性最大的价值在于,React 核心包不需要关心底层渲染目标是什么,它只把容器作为一个不透明的值存下来。

在源码里,containerInfo被多个地方读取。最关键的是commitRoot阶段,React 在完成树构建后,会把真实 DOM 节点挂载到容器上,这就是你看到的container.appendChild(...)之类操作的来源。另外事件系统也依赖这个属性,React 17 之后事件委托绑定在 root container 上,读取containerInfo可以找到事件监听的根节点。

之前排查过一个事件失效问题:组件渲染出来了,但 onClick 没反应。最后发现是事件系统在内部通过containerInfo找 DOM 容器,而项目里在某个高阶组件里替换了容器节点,导致 FiberRoot 里存的还是旧容器。这个属性的排错价值由此可见。

3.3 current——FiberRoot 如何追踪“已提交”的树与并发切换

current是 FiberRoot 上最重要的属性,它保存了对当前已经提交到界面的那棵 Fiber 树的引用。React 17 之前,Fiber 架构区分两棵树:current指向当前渲染完成的树,workInProgress指向正在内存中构建的树。到 React 18 的 Lane 模型下,并发渲染可能同时存在多棵树,但current依旧是指向“已提交”的那一棵。

阅读源码时,你在很多遍历逻辑中都会看到root.current作为起点。比如getPublicRootInstance要从root.current出发沿着child链找第一个没有tag === HostComponent的 fiber;又比如markRootUpdatedscheduleUpdateOnFiber这些更新调度函数,都需要拿到 FiberRoot,然后通过root.current来判断当前树的状态。

在并发更新场景里,current的含义会更加微妙。React 可能已经把一棵新树构建到了一半,但还没有提交,这时候current指向的仍然是旧树。界面展示的是旧树对应的 DOM,新的树还在后台“悬着”。只有等到commitRoot执行完毕,current才被切换到新的树。这就是为什么 React 有“可中断渲染”能力而不会撕裂界面:用户始终看到的是完整的、已提交的树。

实操心得:如果你想在运行时拿到当前 React 应用挂载了几棵树,可以遍历root.currentchild链,再输出每个 fiber 的tagtype。这在写脚手架或者做性能分析工具时非常实用。

3.4 pendingChildren 和 alternate 临时区——提交阶段为什么需要“双缓冲”

pendingChildren这个名字看起来像是“等待中的子节点”,实际上它的作用是临时存放下一次将要提交的 Fiber 树。在completeRoot的过程中,commitRoot会读取root.pendingChildren来执行一些“前提交”阶段的工作,比如调用componentWillUnmount、执行getSnapshotBeforeUpdate等生命周期。

为什么需要这个属性?因为 React 的提交阶段分成了几个子阶段:before mutationmutationlayout。在before mutation阶段,我们需要拿到即将被替换掉的旧树来做清理,同时又要保留新树的引用。React 的解决方案就是把新树先放在pendingChildren里,旧树继续由current指向。等提交完成后,再把current指向新树。

这种“双缓冲”的机制和游戏引擎的交换链很像。你并不直接在用户看到的画面上绘制,而是在离屏缓冲区构建下一帧,等完整了再交换。React 的currentpendingChildren就是这种交换关系。源码里,pendingChildren一般在commitRoot开始阶段被赋值为root.current.alternate,也就是新构建好的树。

3.5 expirationTimes 和 callbackNode——React 调度器如何与 FiberRoot 通信

expirationTimes在 React 17 的调度模型里是核心属性之一,它是一个数组或映射,记录了每个 Lane(在 17 中对应的其实是 expirationTime)对应的到期时间。React 调度器闲置时会去检查当前是否有任务过期,如果过期了就要用同步优先级来处理。到了 React 18,这个属性演化成了root.expirationTimessuspendedLanes等新属性的组合,但核心思想仍然一致:把“谁更紧急”变成可比较的时间值。

callbackNode则更像是 FiberRoot 与调度器间的一根“电源线”。scheduleCallback返回一个调度器内部的回调节点,这个节点会被存到root.callbackNode上。如果后续有更高优先级的任务到来,React 会调用cancelCallback(root.callbackNode)来取消原先的调度回调,再安排新的。你可以把它理解成当前“排好队的任务标识”。

这两个属性联动的场景很典型:点击按钮触发一次 setState,render 阶段被高优先级任务打断,此时root.callbackNode存的是旧任务的调度句柄。React 发现新任务优先级更高,就取消旧回调、安排新回调,同时更新expirationTimes。假如这两个属性没配合好,就会出现“任务丢失”或“死循环调度”的问题。

注意:在 React 18 中,expirationTimes逐渐被lanes相关属性替代,但如果你在维护老项目或者看 17 的源码,这个属性仍然是理解 React 调度队列的钥匙。

3.6 其他容易被忽略的属性:memoizedState、pendingLanes、finishedWork

FiberRoot 上还有一些属性存在感不强,但在特定场景下会卡住你。比如memoizedState,在 React 17 中它被用来保存“当前页面的可视化快照”,getPublicRootInstance通过它来找到 root 对应的 public 实例。这个属性实际上帮助 ReactDOM 内部判断ReactDOM.render在同一个容器上重复调用时的行为。

pendingLanessuspendedLanes是 React 18 中 Lane 模型的核心字段,它们用来记录当前还有哪些更新任务没完成、哪些被挂起。当 Promise 还没有 resolve 时,对应的 lane 会被标记为 suspended;一旦 Promise resolve,React 会发起一次重试。finishedWork则在“渲染完成但尚未提交”的间隙被赋值,它是commitRoot的输入参数。

在实际调试中,查finishedWorkcurrent是否相同,可以判断当前是否处于“有未提交更新”的状态。如果finishedWork不为空而界面没变化,说明卡在了 commit 阶段,需要去查 DOM 操作是否有异常。这种经验在排查 React Native 启动白屏、页面不刷新这类问题时非常有用。

4. 属性联动:一次 setState 从触发到提交,FiberRoot 的完整旅程

这部分我用一个具体例子把这些属性串起来。假设你在一个用ReactDOM.createRoot启动的应用里,点击了一个按钮,触发了一次setState。整个过程中 FiberRoot 上各个属性是怎么变化的?

第一步,事件回调里调用setState,React 会创建一个 update 对象并挂到当前 fiber 的updateQueue上,同时调用scheduleUpdateOnFiber。这个函数会沿着 fiber 的 return 链向上找到 HostRoot,再通过HostRoot.stateNode拿到 FiberRoot。此时root.pendingLanes会被合并进新的 lane,root.callbackNode则用来判断当前是否已经有任务在排队。

第二步,调度器根据优先级选中任务,进入performConcurrentWorkOnRoot。在这里 React 会读取root.callbackNode来判断当前任务的优先级和类型,并从root.current克隆一棵 workInProgress 树开始渲染。这时root.current指向的仍然是旧树,新树正处于构建中。

第三步,渲染完成,进入 commit 阶段。root.finishedWork指向新构建完成的树,root.pendingChildren保存这份待提交树。commit 阶段里,React 会执行getSnapshotBeforeUpdate等钩子,然后通过containerInfo把变更应用到真实 DOM。最后把root.current从旧树切换到新树,并清空finishedWorkpendingChildren

你会发现,FiberRoot 就是整个渲染流程的“交通枢纽”。每个属性在特定阶段被写入,在特定阶段被读取,然后在下个阶段被清空或替换。只要把握住“创建 → 调度 → 渲染 → 提交”这条主线,再去看这些属性,就完全不会乱了。

阶段关键属性读取关键属性写入
调度callbackNodependingLanessuspendedLanespendingLanescallbackNode
渲染currentalternatefinishedWork最终赋值
提交finishedWorkpendingChildrencontainerInfocurrent切换

5. 遗漏细节:不同 React 版本下 FiberRoot 的属性差异与实践建议

React 17 和 React 18 在 FiberRoot 属性设计上有明显的演进。React 17 依赖expirationTime模型,FiberRoot 上有remainingExpirationTimeearliestPendingTimelastPendingTime这类属性;React 18 全面转向 Lane 模型,FiberRoot 上出现了pendingLanessuspendedLanespingedLanes等新字段。如果从源码 git 历史看,这是一个相当大的重构,核心原因是并发渲染需要更精细的位运算优先级处理。

对 React Native 的开发者来说,FiberRoot 的containerInfo指向的是 native 侧的 root tag 或 view 对象,这解释了为什么某些“容器操作”在 Web 和 Native 上表现不同。启动白屏的问题,很多时候和finishedWork是否有值、current是否指向正确树有关。排查 React Native 启动白屏时,你可以断点到commitRoot,检查root.finishedWork是否为空。如果为空,Basic 说明渲染没有完成,问题出在构建阶段;如果不为空而界面没显示,问题出在提交阶段的原生操作上。

实操建议:不要在业务代码里直接引FiberRoot的内部属性,React 从未保证这些内部字段的稳定性。用它来理解原理、辅助排查问题,比直接依赖它的行为要靠谱得多。

6. 总结

FiberRoot 的每个属性都不是孤立的。它的设计本质上是一套“全局状态管理协议”,只不过管理的对象是 React 应用的整个生命周期。tag决定了根的模式,containerInfo连接宿主环境,current追踪已提交的树,pendingChildren为提交阶段缓冲,callbackNodeexpirationTimes连接调度器。当你真正在源码里跟踪过一次更新流程后,这些属性就会从一堆名词变成“老朋友”。

最后分享一个小技巧:在调试 React 源码的时候,可以打开浏览器控制台,找到document.getElementById('root')._reactRootContainer,然后展开里面的_internalRoot,你就能亲眼看到这篇文章里提到的所有属性和它们的当前值。这种“在运行时亲眼验证”的方式,比看任何文章都记得牢。

版权声明: 本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若内容造成侵权/违法违规/事实不符,请联系邮箱:809451989@qq.com进行投诉反馈,一经查实,立即删除!
网站建设 2026/9/18 4:03:11

MiroFish全链路追踪实战:从排障泥潭到轻量级分布式追踪系统落地

从微服务排障的泥潭里爬出来,我越来越觉得“全链路追踪”不是可选项,而是标配。今天想聊聊我最近在用的一个轻量级开源工具MiroFish,它解决的就是分布式环境下一根请求线头找不到、问题定位全靠猜的顽疾。全文不讲虚的,就是一次真…

作者头像 李华
网站建设 2026/9/18 4:03:06

Stolz定理:离散极限计算的核心工具与差分思想

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views …

作者头像 李华
网站建设 2026/9/18 4:00:59

基于 SSM 的校园二手闲置物品交易市场设计与实现

基于 SSM 的校园二手闲置物品交易市场设计与实现 一、前言 每年毕业季,高校都会产生海量的闲置物品:教材、吉他、山地车、小家电……它们大多九成新却只能被低价处理甚至丢弃。与此同时,低年级学生又恰好需要这些高性价比的生活学习用品。缺…

作者头像 李华
网站建设 2026/9/18 4:00:49

Fama-French三因子模型实战:用Python构造因子与回归检验

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views …

作者头像 李华
网站建设 2026/9/18 3:59:00

Anthropic 红队评测多模型,Key 走 TaoToken 行不行?

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views …

作者头像 李华