万字长文介绍React Fiber架构的原理和工作模式
💡
为了写这篇文章,我花了5天时间阅读Fiber的核心源码,尽管本文字符数过万,但相对于几十万行Fiber源码来说,只能算是介绍了Fiber的基础知识,所以如果内容有纰漏,请在评论区为我指正,我会进行更新,如果阅读文章后有哪个关于Fiber的专题你想了解,也可以评论区提出来,我很乐意继续研究源码和分享知识。
自React 16开始,React引入了Fiber架构,解决了以前的更新机制的问题,即在长时间的更新过程中,主线程会被阻塞,导致应用无法及时响应用户输入。本文我们就来聊聊Fiber是什么以及它的底层原理,学习完本文可以让你对Fiber架构的原理有一个比较清晰的认识。
本文首发于我的博客「👉J实验室」
欢迎加入「🌍独立全栈开发交流群」,一起学习交流前端和Node端技术
Fiber是什么?
首先,我们先聊聊React的基本组成:当我们写React组件并使用JSX时,React在底层会将JSX转换为元素的对象结构。例如:
上述代码会被转换为以下形式:
为了将这个元素渲染到DOM上,React需要创建一种内部实例,用来追踪该组件的所有信息和状态。在早期版本的React中,我们称之为“实例”或“虚拟DOM对象”。但在Fiber架构中,这个新的工作单元就叫做Fiber。
所以,在本质上,Fiber是一个JavaScript对象,代表React的一个工作单元,它包含了与组件相关的信息。一个简化的Fiber对象长这样:
当React开始工作时,它会沿着Fiber树形结构进行,试图完成每个Fiber的工作(例如,比较旧的props与新的props,确定是否需要更新组件等)。如果主线程有更重要的工作(例如,响应用户输入),则React可以中断当前工作并返回执行主线程上的任务。
因此,Fiber不仅仅是代表组件的一个内部对象,它还是React的调度和更新机制的核心组成部分。
为什么需要Fiber?
在React 16之前的版本中,是使用递归的方式处理组件树更新,称为堆栈调和(Stack Reconciliation),这种方法一旦开始就不能中断,直到整个组件树都被遍历完。这种机制在处理大量数据或复杂视图时可能导致主线程被阻塞,从而使应用无法及时响应用户的输入或其他高优先级任务。
Fiber的引入改变了这一情况。Fiber可以理解为是React自定义的一个带有链接关系的DOM树,每个Fiber都代表了一个工作单元,React可以在处理任何Fiber之前判断是否有足够的时间完成该工作,并在必要时中断和恢复工作。
Fiber的结构
我们来看一下源码里FiberNode的结构:
其实可以理解为是一个更强大的虚拟DOM。
Fiber工作原理
Fiber工作原理中最核心的点就是:可以中断和恢复,这个特性增强了React的并发性和响应性。
实现可中断和恢复的原因就在于:Fiber的数据结构里提供的信息让React可以追踪工作进度、管理调度和同步更新到DOM
现在我们来聊聊Fiber工作原理中的几个关键点:
-
单元工作:每个Fiber节点代表一个单元,所有Fiber节点共同组成一个Fiber链表树(有链接属性,同时又有树的结构),这种结构让React可以细粒度控制节点的行为。
-
链接属性:child
、sibling
和 return
字段构成了Fiber之间的链接关系,使React能够遍历组件树并知道从哪里开始、继续或停止工作。
-
双缓冲技术:React在更新时,会根据现有的Fiber树(Current Tree)创建一个新的临时树(Work-in-progress (WIP) Tree),WIP-Tree包含了当前更新受影响的最高节点直至其所有子孙节点。Current Tree是当前显示在页面上的视图,WIP-Tree则是在后台进行更新,WIP-Tree更新完成后会复制其它节点,并最终替换掉Current Tree,成为新的Current Tree。因为React在更新时总是维护了两个Fiber树,所以可以随时进行比较、中断或恢复等操作,而且这种机制让React能够同时具备拥有优秀的渲染性能和UI的稳定性。
-
State 和 Props:memoizedProps
、pendingProps
和 memoizedState
字段让React知道组件的上一个状态和即将应用的状态。通过比较这些值,React可以决定组件是否需要更新,从而避免不必要的渲染,提高性能。
-
副作用的追踪:flags
和 subtreeFlags
字段标识Fiber及其子树中需要执行的副作用,例如DOM更新、生命周期方法调用等。React会积累这些副作用,然后在Commit阶段一次性执行,从而提高效率。
Fiber工作流程
了解了Fiber的工作原理后,我们可以通过阅读源码来加深对Fiber的理解。React Fiber的工作流程主要分为两个阶段:
第一阶段:Reconciliation(调和)
- 目标: 确定哪些部分的UI需要更新。
- 原理: 这是React构建工作进度树的阶段,会比较新的props和旧的Fiber树来确定哪些部分需要更新。
调和阶段又分为三个小阶段:
1、创建与标记更新节点:beginWork
- 判断Fiber节点是否要更新:
- 判断Fiber子节点是更新还是复用:
mountChildFibers
和 reconcileChildFibers
最终会进入同一个方法 createChildReconciler
,执行 Fiber 节点的调和(处理诸如新的 Fiber 创建、旧 Fiber 删除或现有 Fiber 更新等操作)。而整个 beginWork
完成后,就会进入 completeWork
流程。
2、收集副作用列表:completeUnitOfWork
和completeWork
completeUnitOfWork
负责遍历Fiber节点,同时记录了有副作用节点的关系。下面从源码上理解它的工作:
completeWork
在 completeUnitOfWork
中被调用,下面是 completeWork
的逻辑,主要是根据 tag 进行不同的处理,真正的核心逻辑在 bubbleProperties
里面
bubbleProperties
为 completeWork
完成了两个工作:
- 记录Fiber的副作用标志
- 为子Fiber创建链表
这两个工作都从下面这段代码中看出来:
调和阶段知识拓展
1、为什么Fiber架构更快?
在上面这段代码里,我们还可以看出来为什么Fiber架构比以前的递归DOM计算要快:flags
或 subtreeFlags
是16进制的标识,在这里进行按位或(|
)运算后,可以记录当前节点本身和子树的副作用类型,通过这个运算结果可以减少节点的遍历,举一个简单的例子说明:
2、调和过程可中断
前面我们提到,调和过程可以被中断,现在我们就看看源码里是怎么进行中断和恢复的。首先,我们要明确可中断的能力是React并发模式(Concurrent Mode)的核心,这种能力使得React可以优先处理高优先级的更新,而推迟低优先级的更新。
可以从下面这段代码理解中断与恢复的处理逻辑:
第二阶段:Commit(提交)
- 目标: 更新DOM并执行任何副作用。
- 原理: 遍历在Reconciliation阶段创建的副作用列表进行更新。
源码里 commitRoot
和 commitRootImpl
是提交阶段的入口方法,在两个方法中,可以看出来提交阶段也有三个核心小阶段,我们一一讲解:
1、遍历副作用列表:BeforeMutation
2、正式提交:CommitMutation
3、处理layout effects:commitLayout
从源码里我们可以看到,一旦进入提交阶段后,React是无法中断的。
结语
以上内容虽无法覆盖Fiber的方方面面,但可以确保你学完后对Fiber会有一个整体上的认识,并且让你在以后阅读互联网上其它关于Fiber架构的文章时,不再因为基础知识困惑,而是能够根据已有的思路轻松地拓展你大脑里关于Fiber架构的知识网。