2026-04-19 04:04:56
要说React框架这些年迭代更新中让人眼前一亮的方案设计,FiberReconciler(下文将简称为Fiber)绝对占有一席之地。作为React团队两年多研究与后续不断深入所产出的成果,Fiber提高了React对于复杂页面的响应能力和性能感知,使其在面对不断扩展的页面场景时可以更加流畅的渲染。今天我们一起从Reconciler这个概念开始,简单聊聊ReactFiber。
Reconciler在调度什么?在某一时间节点调用React的render()方法,会创建一棵由React元素组成的树。在下一次state或props更新时,相同的render()方法会返回一棵不同的树。React需要基于这两棵树之间的差别来判断如何高效的更新UI,以保证当前UI与最新的树保持同步。
这是React历代Reconciler的设计动机,也是React团队优化方向的主旨。合理的分配浏览器每次渲染内容,保证页面的及时更新,正是Reconciler的职责所在。
Fiber的前任:StackReconcilerStackReconciler(下文将简称为Stack)作为Fiber的前任调度器,就像它的名字一样,通过栈的方式实现任务的调度:将不同任务(渲染变动)压入栈中,浏览器每次绘制的时候,将执行这个栈中已经存在的任务。
说到这,Stack的问题已经很明显的暴露出来了。我们知道设备刷新频率通常为60Hz,如今支持高刷(120Hz+)的设备也在不断增加,页面每一帧所消耗掉的时间也在不断减少1s/60↑≈16ms↓,在这段时间内,浏览器需要执行如下任务
浏览器的1帧
可用户并不关心上面的大部分流程,只需要页面可以及时的展示就足够了。如果我们在一次渲染时,向栈中推入了过多的任务,从而导致其执行时间超过浏览器的一帧,就会使这一帧没能及时响应渲染页面,也是就我们常说的掉帧。
而Stack这种架构的特点就是,所有任务都按顺序的压入了栈中,而执行的时候无法确认当前的任务是否会耗去过长的脚本运行时间,使得这一帧时间内里浏览器能做的事不可控。
所以可控便成了React团队的优化方向,FiberReconciler应运而生。
Fiber的诞生其实Fiber这一概念并非由React定义。Fiber本义为纤维,在计算机科学中含义为纤程,是一种轻量级的执行线程。
线程,操作系统能够进行运算调度的最小单位。
这里不必为纤程、线程、x程...等的定义所感到迷惑,从下图的定义看出:对于不同的调度方,相同的线程类型会有不同的名字。
结合定义与上图我们可以知道fiber的特性:“轻量级与非抢占式”
非抢占式,也叫协作式(Cooperative),是一种多任务方式,相对于抢占式多任务(Preemptivemultitasking),协作式多任务要求每一个运行中的程序,定时放弃自己的运行权利,告知操作系统可让下一个程序运行。
React团队的目标也是与此一致,通过管理子任务的调用和让出,来决定当前的运行时处理哪部分内容:浏览器需要进行渲染时,线程让出,当前任务挂起。等到资源被释放回来的时候,又恢复执行,通过合理使用资源实现了多任务处理。
Fiber实现思路为了完成上述的目标,React团队通过在Stack栈的基础上进行数据结构调整,将之前需要递归进行处理的事情分解成增量的执行单元,最终得出的实现方式就是链表。
链表相较于栈来说操作更高效,对于顺序调整、删除等情况,只需要改变节点的指针指向就可以,在多向链表中,不仅可以根据当前节点找到下一个节点,还可以找到他的父节点或者兄弟节点。但链表由于保存了更多的指针,所以站来说将占用更多的空间。
在React项目的/packages/react-reconciler/src/ReactInternalTypes.js文件中,有着Fiber单元的定义,每一个VirtualDOM节点内部现在使用Fiber来表示。
exporttypeFiber={tag:WorkTag,key:null|string,...//链表结构信息return:Fiber|null,child:Fiber|null,sibling:Fiber|null,...}前面提到,Stack是基于栈,每一次更新操作会一直占用主线程,直到更新完成。这可能会导致事件响应延迟,动画卡顿等现象。
在Fiber机制中,它采用"化整为零"的战术,将Reconciler开始调度时,将递归遍历VDOM这个大任务分成若干小任务,每个任务只负责一个节点的处理。
在处理当前任务的时候生成下一个任务,如果此时浏览器需要执行渲染动作,则需要进行让出线程。如果没有下一个任务生成了,则本次渲染操作完成。
Fiber的线程控制至于React是如何进一步实现线程控制的,开发团队在官方文档中的设计原则这样写道:
我们认为React在一个应用中的位置很独特,它知道当前哪些计算当前是相关的,哪些不是。
如果不在当前屏幕,我们可以延迟执行相关逻辑。如果数据数据到达的速度快过帧速,我们可以合并、批量更新。我们优先执行用户交互的工作,延后执行相对不那么重要的后台工作,从而避免掉帧。
遵从上述原则,从上我们可以了解到,线程控制离不开保持帧的渲染,所以在实现方案上很自然的就想到requestAnimationFrame这个API,与之相关的还有requestIdleCallback。
requestIdleCallback方法插入一个函数,这个函数将在浏览器空闲时期被调用。这使开发者能够在主事件循环上执行后台和低优先级工作,而不会影响延迟关键事件。
若使用这两个API,此时Fiber的任务调度如下图所示
使用rAF的任务调度
看起相当完美,requestIdleCallback仿佛是因此而生一般,Fiber的早期版本确实却是使用了这样的方案,不过这已经是过去式了。在19年的一次更新中,React团队推翻之前的设计,使用了MessageChannel来实现了对于线程控制。
MessageChannel允许我们创建一个新的消息通道,并通过它的两个MessagePort属性发送数据。此特性在WebWorker中可用。其使用方式如下:
constchannel=newMessageChannel()channel.port1.onmessage=function(msgEvent){console.log('recievemessage!')}channel.port2.postMessage(null)//output:recievemessage!React开发成员对这次更新这样说道:requestAnimationFrame过于依赖硬件设备,无法在其之上进一步减少任务调度频率,以获得更大的优化空间。使用高频(5ms)少量的消息事件进行任务调度,虽然会加剧主线程与其他浏览器任务的争用,但却值得一试。
Reactcommitmessage
在最新版本源码的/packages/scheduler/src/forks/Scheduler.js文件中可以看到,这次“尝试性实验”沿用至今。
letschedulePerformWorkUntilDeadline;//调度器if(typeoflocalSetImmediate==='function'){//Node.js与旧版本IE环境....}elseif(typeofMessageChannel!=='undefined'){//DOM与WebWorker环境.constchannel=newMessageChannel();constport=channel.port2;channel.port1.onmessage=performWorkUntilDeadline;//执行器schedulePerformWorkUntilDeadline=()=>{port.postMessage(null);};}else{//非浏览器环境的兜底方案....}这里我们只看第二种情况就好,React将上述的集中兼容处理做一封装,最终得到一个与requestIdleCallback类似的函数requestHostCallback
functionrequestHostCallback(callback){scheduledHostCallback=callback;//开启任务循环if(!isMessageLoopRunning){isMessageLoopRunning=true;//调度器开始运作,即port1端口将收到消息,执行performWorkUntilDeadlineschedulePerformWorkUntilDeadline();}}我们接着看performWorkUntilDeadline如何处理事件的
constperformWorkUntilDeadline=()=>{//当前是否有处理中的任务if(scheduledHostCallback!==null){//计算此次任务的deadlineconstcurrentTime=getCurrentTime();deadline=currentTime+yieldInterval;consthasTimeRemaining=true;lethasMoreWork=true;try{//是否还有更多任务hasMoreWork=scheduledHostCallback(hasTimeRemaining,currentTime);}finally{if(hasMoreWork){//有:继续进行任务调度schedulePerformWorkUntilDeadline();}else{isMessageLoopRunning=false;scheduledHostCallback=null;}}}else{isMessageLoopRunning=false;}};整个流程可以用下图表示
任务调度的初步模型已经有了,紧接着我们来看Fiber是如何把控线程的让出:
constlocalPerformance=performance;//获取当前时间getCurrentTime=()=>localPerformance.now();//让出线程周期,默认是5msletyieldInterval=5;letdeadline=0;constmaxYieldInterval=300;letneedsPaint=false;constscheduling=navigator.scheduling;//是否让出主线程shouldYieldToHost=function(){constcurrentTime=getCurrentTime();if(currentTime>=deadline){if(needsPaint||scheduling.isInputPending()){//判断是否有输入事件returntrue;}returncurrentTime>=maxYieldInterval;//在持续运行的react应用中,currentTime肯定大于300ms,这个判断只在初始化过程中才有可能返回false}else{//当前帧还有时间returnfalse;}};当currentTime>=deadline时,我们将会让出主线程(deadline的计算在performWorkUntilDeadline中)yieldInterval默认是5ms,如果一个task运行时间超过5ms,那么在下一个task执行之前,将会把控制权归还浏览器,以保证浏览时的及时渲染。
任务的恢复接下来我们看一下由于让出线程所被中断的任务如何恢复。
代码中定义了一个任务队列:
//Tasksarestoredonaminheap//任务被存储在一个小根堆中vartaskQueue=[];//任务队列通过unstable_scheduleCallback进行任务创建
//代码有所简化functionunstable_scheduleCallback(priorityLevel,callback,options){//【1.计算任务过期时间】varstartTime=getCurrentTime();vartimeout;switch(priorityLevel){...timeout=SOME_PRIORITY_TIMEOUT}varexpirationTime=startTime+timeout;//优先级越高;过期时间越小//【2.创建新任务】varnewTask={id:taskIdCounter++,//唯一IDcallback,//传入的回调函数priorityLevel,//优先级startTime,//创建task的时间expirationTime,//过期时间,};newTask.sortIndex=expirationTime;//【3.加入任务队列】push(taskQueue,newTask);//【4.请求调度】if(!isHostCallbackScheduled&&!isPerformingWork){isHostCallbackScheduled=true;requestHostCallback(flushWork);}returnnewTask;}可以看到,在上面代码中的【4.请求调度】中使用了上面提到的requestHostCallback方法,也正是postMessage的开始。requestHostCallback的入参flushWork实际上返回的是一个函数workLoop。所以我们从workLoop继续看:
//代码有所简化functionworkLoop(hasTimeRemaining,initialTime){letcurrentTime=initialTime;//保存当前时间,用于判断任务是否过期currentTask=peek(taskQueue);//获取队列中的第一个任务while(currentTask!==null){//是否需要让出线程的判断if(currentTask.expirationTime>currentTime&&(!hasTimeRemaining||shouldYieldToHost())){//任务虽然没有超时,但本帧时间不够了。挂起break;}constcallback=currentTask.callback;if(typeofcallback==='function'){currentTask.callback=null;currentPriorityLevel=currentTask.priorityLevel;constdidUserCallbackTimeout=currentTask.expirationTime<=currentTime;//执行回调constcontinuationCallback=callback(didUserCallbackTimeout);currentTime=getCurrentTime();//回调完成,判断是否还有连续回调if(typeofcontinuationCallback==='function'){currentTask.callback=continuationCallback;}else{//把currentTask移出队列if(currentTask===peek(taskQueue)){pop(taskQueue);}}}else{//如果任务被取消(这时currentTask.callback=null),将其移出队列pop(taskQueue);}//更新currentTaskcurrentTask=peek(taskQueue);}if(currentTask!==null){returntrue;//如果task队列没有清空,返回ture.等待调度中心下一次回调}else{returnfalse;//task队列已经清空,返回false.}}就这样,在一次次的workLoop循环中,通过向currentTask的赋值,Fiber始终保存着当前任务的执行情况,以根据不同的deadline及时中断,保存,再通过下一次的unstable_scheduleCallback恢复任务调度。
可以看出,使用postMessage实现的任务调度流程整体更加可控,对其他因素的依赖更少。虽说开发者对这次改动并无感知,但其背后的设计思路值得我们学习。至于Fiber下一次会有怎样的更新,我们拭目以待。
结语关于Fiber的介绍先告一段落,希望今天的你能有所收获。
欢迎在评论区留下你的建议或问题,也欢迎指出文中的错误。奇葩说框架系列后续将持续更新,感兴趣的小伙伴们可以不要忘了关注我们~
参考文章reactjs.org/docs/design-principles.html
www.yuque.com/docs/share/8c167e39-1f5e-4c6d-8004-e57cf3851751
github.com/7kms/react-illustration-series/blob/master/docs/main/scheduler.md
react.jokcy.me/book/flow/scheduler-pkg.html