当前位置: 首页 > Web前端 > HTML

框架作者视角谈:React调度算法的迭代过程

时间:2023-03-27 23:53:00 HTML

大家好,我是Kason。React内部最难理解的就是调度算法,不仅抽象复杂,而且还要重构一次。可以说只有React团队才能完全理解这个算法。在这种情况下,本文试图从React团队成员的角度来谈谈调度算法。欢迎加入人类优质前端框架群。什么是调度算法?React在v16之前面临的主要性能问题是:当组件树非常大时,更新状态可能会导致页面卡顿。根本原因是:更新过程是同步的,不可中断的。为了解决这个问题,React提出了Fiber架构,旨在让更新过程异步和可中断。最终的交互过程是这样的:不同的交互产生不同优先级的更新(比如onClick回调中的更新优先级最高,useEffect回调中触发的更新优先级平均)。调度算法从众多更新中选择一个优先级作为本次。render的优先级以第2步选择的优先级渲染组件树,在渲染过程中,如果再次触发交互过程,在第2步选择了更高的优先级,则中断之前的渲染,并以新的优先级重新开始渲染。本文要讲的是第2步中的调度算法。expirationTimeSchedulingAlgorithm调度算法需要解决的最基本的问题是:如何选择其中一个update的优先级作为本次render的优先级?最早的算法称为expirationTime算法。具体更新优先级与当前触发交互的时间和优先级对应的延迟时间有关://MAX_SIGNED_31_BIT_INT最大为31位Intergerupdate.expirationTime=MAX_SIGNED_31_BIT_INT-(currentTime+updatePriority);例如高优先级更新u1,低优先级更新u2的updatePriority分别为0和200,则MAX_SIGNED_31_BIT_INT-(currentTime+0)>MAX_SIGNED_31_BIT_INT-(currentTime+200)//即u1.expirationTime>u2.expirationTime;意味着u1具有更高的优先级。expirationTime算法的原理简单易懂:每次都选择所有更新中优先级最高的。如何表示“batch”另外,还有一个问题需要解决:如何表示batch?什么是批次?考虑以下示例://定义状态numconst[num,updateNum]=useState(0);//...某处修改num//修改方式1updateNum(3);//修改方式2updateNum(num=>num+1);两种修改状态的方法都会创建一个更新,不同的是:第一种方法不需要考虑更新前的状态,直接修改状态num为3第二种方法需要根据更新前的状态计算新状态由于第二种方式,更新之间可能存在连续性。因此,调度算法计算出一个优先级后,真正参与组件渲染时当前状态值计算的是:计算出的优先级对应更新+与该优先级相关的其他优先级对应更新。这些是相互关联的,连续更新称为批处理。expirationTime算法计算批次的方式也很简单粗暴:优先级大于某个值(priorityOfBatch)的更新会被归为同一批次。constisUpdateIncludedInBatch=priorityOfUpdate>=priorityOfBatch;expirationTime算法确保呈现是异步可中断的,并且始终首先处理具有最高优先级的更新。此功能在此期间称为异步模式。IO密集场景下的AsyncMode可以解决以下问题:组件树逻辑复杂导致更新时卡顿(因为组件渲染变得可中断),重要交互响应更快(因为不同交互产生不同优先级的更新).这些问题统称为CPU密集型问题。在前端,还有一类问题也会影响体验,那就是请求数据带来的等待。此类问题称为IO绑定问题。为了解决IO密集型问题,React提出了Suspense。考虑以下代码:constApp=()=>{const[count,setCount]=useState(0);useEffect(()=>{constt=setInterval(()=>{setCount(count=>count+1);},1000);return()=>clearInterval(t);},[]);返回(<>loading...

}>
countis{count}
);};其中:每秒会触发一次更新,状态count会更新为count=>count+1,在Sub中会发起一个异步请求。在请求返回之前,包裹在Sub中的Suspense将渲染fallback。假设三秒后请求返回,理想情况下,请求前后的UI会依次显示://Sub中的请求发起前我是sub,count为0
countis0
//RequestinSub发起第一秒
loading...
countis1
//Sub中的request在第二秒发起
loading...
countis2
//Sub中的请求在第三秒发起
loading...
countis3
//Sub中请求成功后我是sub,请求成功s,countis4
countis4
从用户的角度来看,有两个任务在并发执行:请求Sub的任务(观察第一个div的变化)和changingcount(观察第二个div的变化)Suspense带来多任务并发执行的直观感受。因此,AsyncMode(异步模式)也改名为ConcurrentMode(并发模式)。无法解决的bug,那么更新对应的Suspense优先级是高还是低呢?当请求成功时,合理的逻辑应该是尽快显示成功的UI。所以Suspense对应的更新应该是高优先级的更新。然后,示例中有两种更新:对应Suspense的高质量IO更新,记为u0,每秒产生的低质量CPU更新,记为u1、u2、u3等。在expirationTime下algorithm://u0的优先级远高于u1,u2,u3...u0.expirationTime>>u1.expirationTime>u2.expirationTime>...u0的优先级最高,所以u1和后续更新需要等待u0结束执行。而u0需要等待请求完成后才能执行。因此发起请求前后,UI会依次显示为://Sub中请求发起前我是sub,count为0
countis0
//Sub在请求发起的第1秒
loading...
countis0
//Sub在请求发起的第1秒second
loading...
countis0
//第3秒发起Sub中的请求
loading...
countis0
//Sub中请求成功后我是sub,请求成功,count为4
count为4
从用户的角度来看,第二个div是卡住3秒然后突然变成4秒。因此,当只考虑CPU密集型场景时,先执行高优先级更新的算法是没有问题的。但是,考虑到IO密集型场景,高质量的IO更新会阻塞低质量的CPU更新,这显然是错误的。所以expirationTime算法不能很好地支持并发更新。expirationTime算法在线demo出现bug的原因是expirationTime算法最大的问题在于expirationTime字段耦合了priority和batch这两个概念,限制了模型的表达能力。这导致高质量的IO更新不会与低质量的CPU更新归为同一批次。那么低质量的CPU更新必须要等到高质量的IO更新处理完毕后才能处理。如果不同的更新可以根据实际情况灵活分批,就不会出现这个bug。重构迫在眉睫,重构的目标很明确:将priority和batch拆分为两个字段。车道调度算法新的调度算法称为车道。他如何定义优先级和批次?对于优先级,一个lane是一个32bit的Integer,最高位是符号位,所以最多可以有31位参与运算。不同优先级对应不同lane,越低的位代表越高的优先级,比如://对应SyncLane,为最高优先级0b0000000000000000000000000000001//对应InputContinuousLane0b0000000000000000000000000000100//对应DefaultLane0b0000000000000000000000000010000//对应IdleLane0b0100000000000000000000000000000//对应OffscreenLane,为最低优先Level0b1000000000000000000000000000000batchesaredefinedbylanes,andalanesisalsoa32bitInteger,representingacollectionofoneormorelanes.Multiplelanescanbeeasilyassignedtothesamebatchwithbitoperations://BatchtobeusedletlanesForBatch=0;constlaneA=0b0000000000000000000000001000000;constlaneB=0b000000000000000000000000000001;//IncludelanesForBatchinthebatch|=laneA;//IncludelaneBinthebatchlanesForBatch|=laneB;TheSuspensebugmentionedaboveiscausedbytheexpirationTimealgorithmnotbeingabletoflexiblydefinebatches.Laneshasnosuchconcernsatall,andanypriority(lane)thatwantstobedesignatedasthesamebatchcanbeeasilyhandledwithbitoperations.LaneAlgorithmOnlineDemoSummaryTheschedulingalgorithmhastosolvetwoproblems:theexpirationTimefieldusedinthepriorityselectionbatchexpirationTimealgorithmcouplesthesetwoconcepts,resultingininflexibility.TheemergenceoftheLanealgorithmsolvestheaboveproblems.