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

前端实训:React调度算法迭代过程

时间:2023-03-28 19:00:58 HTML

React内部最难理解的就是“调度算法”,不仅抽象复杂,而且还要重构一次。可以说只有React团队才能完全理解这个算法。在这种情况下,本文试图从React团队成员的角度来谈谈“调度算法”。什么是调度算法?React在v16之前面临的主要性能问题是:当组件树非常大时,更新状态可能会导致页面卡顿。根本原因在于更新过程是“同步且不可中断”的。为了解决这个问题,React提出了Fiber架构,意在“让更新过程异步且可中断”。最终的交互过程如下:不同的交互产生不同优先级的更新(比如onClick回调中的更新优先级最高,useEffect回调中触发的更新优先级平均)。“调度算法”从众多更新中选择一个优先级作为本次渲染的优先级,即渲染步骤2中选择的优先级的组件树。在渲染过程中,如果再次触发交互过程,则选择更高的优先级在第2步中,先前的渲染将被中断,并使用新的优先级。关卡重新开始渲染。本文要讲的是第2步中的“调度算法”。expirationTimeSchedulingAlgorithm“调度算法”需要解决的最基本的问题是:如何从许多前端培训更新作为此渲染的优先级?最早的算法称为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。过期时间;意味着u1具有更高的优先级。expirationTime算法的原理简单易懂:每次都选择所有更新中“优先级最高”的那个。如何表示“batch”此外,还有一个问题需要解决:如何表示“batch”?什么是“批次”?考虑以下示例://定义状态numconst[num,updateNum]=useState(0);//...某处修改num//修改方式1updateNum(3);//修改方式2updateNum(num=>num+1);两种“修改状态的方法”都会创建更新,不同的是:第一种方法不需要考虑更新前的状态,直接修改状态num为3第二种方法需要根据“更新”Previousstate”来计算新状态由于第二种方法的存在,更新之间可能存在连续性。因此,“调度算法”计算出一个优先级后,实际参与计算“当前状态值”的是什么时候组件渲染的是:“计算出的优先级的对应更新”+“与该优先级相关的其他优先级的对应更新”这些相互关联的、顺序的更新被称为一个“批次”。expirationTime算法计算“批次”的方式也很简单,crude:优先级大于某个值(priorityOfBatch)的更新会被归为同一个batchconstisUpdateIncludedInBatch=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);},[]);return(<>Suspensefallback={

loading...
}>
countis{count}
/>);};其中:每秒触发一次update,statuscount更新为count=>count+1。Sub中会发起一个异步请求,在请求返回前,包裹Sub的Suspense会渲染fallback假设三秒后请求返回。理想情况下,请求发起前后的UI会依次显示://Sub中的请求发起前我是sub,count为0
countis0
//Sub中的请求是第1秒发起的
loading...
countis1
//Sub中的请求是第2秒发起的second
loading...
countis2
//RequestinSub发起3rdsecond
loading...
countis3
//Sub中请求成功后我是sub,请求成功,count为4
count为4
从用户的角度来看,有两个任务正在并发执行:请求Sub的任务(观察第一个div)的变化改变count的任务(观察第二个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需要等待“requestcompleted”后才能执行。因此发起请求前后,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字段耦合了“优先级”和“批次”两个概念,限制了模型的表达能力。这会导致高质量IO更新不会与低质量CPU更新分到同一“批次”中。那么低质量的CPU更新必须要等到高质量的IO更新处理完毕后才能处理。如果不同的更新可以根据实际情况灵活分“批”,就不会出现这个bug。重构迫在眉睫,重构的目标很明确:拆分“优先级”和“批次”两个字段。车道调度算法新的调度算法称为车道。他如何定义“优先级”和“批次”?对于优先级,一个lane是一个32bit的Integer,最高位是符号位,所以最多可以有31位参与运算。不同优先级对应不同lane,越低的位表示越高级的优先,比如://对应SyncLane,为最高优先级0b0000000000000000000000000000001//对应InputContinuousLane0b0000000000000000000000000000100//对应DefaultLane0b0000000000000000000000000010000//对应IdleLane0b0100000000000000000000000000000//对应OffscreenLane,为最低优先Level0b1000000000000000000000000000000"batch"isdefinedbylanes,andalaneisalsoa32bitInteger,representing“一个或多个车道的集合”。可以使用位操作轻松地将多个通道分配给同一个批次://要使用的批次letlanesForBatch=0;constlaneA=0b00000000000000000000000010000000;constlaneB=0b0000000000000000000000000000001;//在批次中包含lanesForBatch|=laneA;//在批处理中包含laneBlanesForBatch|=laneB;上面提到的Suspensebug是由于expirationTime算法不能灵活定义batches导致的。Lanes则完全没有这样的顾虑,任何一个优先级(lane)想要被指定为同一个“批次”,用位运算就可以轻松搞定。文章来自魔术师卡松