通俗易懂的React原理(一):fiber架构

开个小坑,记录自己学习React源码过程中的一些心得。感觉市面上讲解React源码的书籍、视频都讲的一般,颇有照本宣科的意思。我希望能用通俗的话,把自己对React的理解记录下来。一方面是自己遗忘了之后,能回来看看,另一方面也是希望帮助到其他希望了解React源码,却又摸不着门道的人

第一篇我们首先来讲一讲Fiber架构。所谓Fiber架构,是React16开始的新架构。说到虚拟DOM,相信大家都不陌生。所谓Fiber,简单来说就是通过链表来实现的虚拟Dom tree。你写的React组件最终会被转换成Fiber Node,通过指针相连,最终得到一个虚拟Dom tree,或者我们也可以叫它叫做Fiber tree。

Fiber tree的结构

一个简单的Fiber tree可能就如上图所示,A~F是tree上面的Fiber Node,箭头则是将他们连接起来的指针。我们可以看到,每个Fiber Node最多有三条指针指向其他Fiber Node,分别是指向子节点(child,即第一个孩子),指向右侧兄弟(sibling),和指向父节点(return)。

在Reconciliation 阶段,React会通过遍历节点的方式,去构建Fiber tree。在遍历Fiber tree的时候,React会沿着我标的数字顺序去遍历,并且每个节点一去一回,会遍历两次(后面会讲到,分别是beginWork和completeWork)。

这个遍历有点像深度优先搜索。用文字概括一下遍历的顺序,先从根节点开始,正向遍历。对于任何一个节点,正向遍历它之后,都会继续正向遍历他的子节点(例如,1,2,4)。当没有子节点,即当前是叶子结点的时候,立刻反向遍历自己(未在图中标出)。然后去正向遍历当前节点的兄弟节点及其子节点(例如,3,6)。如果没有兄弟节点了,那么反向遍历父节点(例如,5,7,8)。

新老架构

Stack架构的不可中断

Fiber架构,是React16新推出的架构。更早的React版本,是Stack架构,即通过堆栈(递归),去完成虚拟Dom tree的构建。而递归就有一个问题,它是不可中断的。一旦中断,就没办法从上一次中断的地方继续构建,因为之前层层递归的函数调用栈已经被销毁了,找不回来上一次递归进行到了哪。

所以如果当构建虚拟Dom tree花费的时间比较久的时候,就会一直占用主线程,导致浏览器无法渲染下一帧,让用户感觉到卡顿。

Fiber架构是可中断的

相比之下,Fiber架构通过链表实现,然后去遍历链表,则可以在每遍历到一个节点的时候,判断一下是否需要中断。

如果要中断,那我用一个全局变量保存一下当前遍历到那个节点就好了。恢复的时候,我再从保存的节点继续遍历。

例如下图,将已经遍历过的节点用红色表示。假如我刚正向遍历到D,就被中断了(可能是因为已经占用了主线程太久了),那就停止遍历,记录一下我正在遍历D。等继续的时候,我就可以从D开始继续遍历

举一道面试题的例子

碰巧我跟朋友交流,她刚遇到了一道面试题,我觉得就很像React的这个中断机制。如果单纯的语言描述难以理解的话,不妨用一个简单的场景来类比吧

1
2
要求:实现从1~n的累加。为了避免长时间阻塞主线程,需要每15ms中断一次。
将后续数字的累加放到宏任务队列当中继续执行。最终返回一个promise

这道题如何来实现呢,首先累加,我们最优先想到是循环,然后题目要求每15ms中断一次。那我们可以每次遍历,都判断一下当前已经执行多久了。如果超过15ms,就记录一下当前的进度,然后通过setTimeout(fn, 0)来使得在下一次事件循环执行宏任务的时候,继续刚才的任务。

最后,题目要求返回一个promise。那我们最后肯定是要new一个Promise,然后return这个promise的。当累加完毕的时候,使这个promise resolve即可。

参考实现如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
const sum = (start, end)=>{
let resolve, result = 0
const helper = (start, end)=>{
const startTime = Date.now()
console.log(startTime)
for(let i=start; i<=end; i++){
if(Date.now() - startTime > 15){
setTimeout(()=>{
helper(i, end)
}, 0)
return
}
result += i
}
resolve(result)
}

return new Promise((res)=>{
resolve = res
helper(start, end)
})
}

sum(1, 10).then(res=>{
console.log(res)
})

React的遍历,你也可以简单理解成这样。React在16的时候改成Fiber架构,就是为了可以在虚拟Dom tree构建到一半的时候,中止构建,结束js的执行,让主线程转而去渲染。然后等下一次事件循环,再继续去构建虚拟Dom tree。

虽然React16就转到了Fiber架构,但是这个版本并不会中断Fiber tree的构建,而是依旧会一直遍历,直到完整的Fiber tree被构建完毕。所以React16,17更像是过渡的版本,从原本的Stack架构迁移过来。

直到React18开始,React才正式启用了Concurrent模式。即在使用特定API的时候,根据是否阻塞主线程过久,在构建Fiber tree的时候可能会中断,并且可能有多个更新的流程并发执行。具体的细节我们后面源码部分再讲,前期主要做铺垫,有一个初步的了解。

Fiber的双缓冲更新

React维护了两份Fiber tree,一份叫做current,一份叫做workInProgress。记住这两个变量名,它们在后面的源码里面会经常出现。

这两份tree也不是永恒不变的,而是会互相切换,类似于显卡的前缓冲区和后缓冲区。current是当前被渲染出来的Fiber tree,即前缓冲区。而workInProgress则是重新渲染的时候,正在构建中的Fiber tree,即后缓冲区。

当workInProgress被构建完毕时,workInProgress和current身份互换。这样维护两份,而不是直接在原来一份上面更新,是为了避免渲染出修改了一半的Fiber tree。

值得注意的是,workInProgress和current两颗tree中的对应节点,也有指针互相指着。即workInProgress.alternate = current ,且current.alternate = workInProgress。由于重新渲染的时候经常需要复用上一次渲染的部分数据,所以有alternate会方便很多,可以拿到这个Fiber Node上一次渲染时候的一些状态

当正在构建workInProgress时,如图。对于下图,需要补充说明的是FiberRootNode和HostRootFiber。FiberRootNode并不属于Fiber tree当中,它管理着两颗Fiber tree,并且通过current来指向current tree,可以把它看做React应用的大本营。而HostRootFiber则是根节点

而当workInProgress构建完成,并且提交给Renderer渲染完毕后,FiberRootNode的current指向也发生了切换,然后current和workInProgress也就互换了

扩展资料

Lin Clark 在React Conf 2017上对fiber的介绍


通俗易懂的React原理(一):fiber架构
https://miku03090831.github.io/2025/07/25/通俗易懂的React原理(一):fiber架构/
作者
qh_meng
发布于
2025年7月25日
许可协议