本文转载自微信公众号《ELab团队》,作者ELab.lijiayu。如需转载本文,请联系ELab团队公众号。一、前言Elab掘金:ReactFiber架构解析[1]对ReactFiber架构的实现进行了分析。React内部实现了方法requestIdleCallback,即一帧idle执行任务,但是Schedule+Lane模式远比requestIdleCallback复杂。这里我们先了解一下requestIdleCallback是干什么的,然后尝试通过requestAnimationFrame+MessageChannel来模拟React实现一帧idle判断。2.requestIdleCallbackwindow.requestIdleCallback()[2]2.1概念理解图:简述框架生命周期RequestIdleCallback简单来说,如果判断一个框架有空闲时间,它就会执行某个任务。目的是为了解决当任务需要长时间占用主进程,导致更高优先级的任务(如动画或事件任务)无法及时响应时,出现页面丢帧(卡死)的问题。因此,RequestIdleCallback定位处理为:不重要、非紧急的任务。RequestIdleCallback参数说明:window.requestIdleCallback(callback[,options]);callback是要执行的回调函数,这个函数会接收deadline作为一个对象。//回调函数接收deadlinetypeDeadline={timeRemaining:()=>number//当前剩余可用时间。即帧的剩余时间。didTimeout:boolean//是否超时。}//接收回调任务typeRequestIdleCallback=(cb:(deadline:Deadline)=>void,options?:Options)=>number2.2实现demorequestIdleCallback处理任务说明:Demo:https://linjiayu6.github.io/FE-RequestIdleCallback-demo/Github:RequestIdleCallbackExperiment[3]constbindClick=id=>element(id).addEventListener('click',Work.onAsyncUnit)//绑定点击事件bindClick('btnA')bindClick('btnB')bindClick('btnC')varWork={//有10000个任务unit:10000,//处理单个任务需要如下处理onOneUnit:function(){for(vari=0;i<=500000;i++){}},//处理任务onAsyncUnit:function(){//空闲时基为1msconstFREE_TIME=1//执行到任务数let_u=0functioncb(deadline){//当任务还没有处理完&一帧是stillfreeTime>1mswhile(_uFREE_TIME){Work.onOneUnit()_u++}//任务完成,执行回调if(_u>=Work.unit){//执行回调返回}//如果任务没有完成,继续等待idle执行window.requestIdleCallback(cb)}window.requestIdleCallback(cb)}}以上就是window.requestIdleCallback的实现过程。核心:即当一个框架空闲时,浏览器执行一个低优先级的任务。2.3缺陷可能偏离主题:requestIdleCallback每秒仅调用20次-我的6x2内核Linux机器上的Chrome,它对UI工作并不是很有用。requestAnimationFrame被更频繁地调用,但特定于顾名思义的任务。[4]实验api,兼容性一般。实验结论:requestIdleCallbackFPS仅为20ms,正常情况下一帧渲染时间控制在16.67ms(1s/60=16.67ms)。这个时间比平滑页面的要求要高。个人看法:RequestIdleCallback不重要也不紧急。因为React呈现内容,所以它不是不重要和不紧急的。不仅api兼容,而且帧渲染能力一般,达不到渲染要求,所以React团队自己实现了。3.ReactrequestIdleCallback实现实验要实现requestIdleCallback的处理,需要解决两点:When:如何判断一个frame是否空闲?where:如果有空闲时间,在一个帧的哪个位置执行任务?3.1requestAnimationFrame计算1帧过期时间requestAnimationFrame[5]由系统决定执行回调函数。它将所有DOM操作集中在每一帧中,在一次重绘或回流中完成,重绘或回流的时间间隔紧跟屏幕的刷新率,不会造成丢帧和卡顿。浏览器刷新率为60Hz,一帧渲染时间控制在16.67ms(1s/60=16.67ms)。DOMHighResTimeStamp[6]requestAnimationFrame的参数如下//回调函数接收rafTime开始执行一帧开始时间//接收回调任务typeRequestAnimationFrame=(cb:(rafTime:number)=>void)计算时间点当一帧过期。//计算当前帧的结束时间vardeadlineTime;window.selfRequestIdleCallback=function(cb){requestAnimationFrame(rafTime=>{//结束时间=开始时间+一帧时间16.667msdeadlineTime=rafTime+16.667//......})}上面使用requestAnimationFrame来计算结束时间点。暂时先把空闲时间的判断放在后面解决。我们先来看一下,在时间充裕的情况下,什么时候执行某项任务。3.2MessageChannel宏任务执行任务MessageChannel()[7]MessageChannel创建一个通信管道,这个管道有两个端口,每个端口都可以通过postMessage发送数据,而一个端口只要绑定onmessage回调方法就可以接收数据从另一个港口。在看方法的实现之前,你可能会有疑问:为什么要用宏任务处理?核心是放弃主进程,让浏览器更新页面。利用事件循环机制,在下一帧宏任务中执行未完成的任务。为什么不是微任务?走远。对于一个事件循环机制,所有的微任务都会在页面更新之前执行,所以无法达到将主线程让给浏览器的目的。既然使用了宏任务,为什么不用setTimeout宏任务来执行呢?如果不支持MessageChannel,则使用setTimeout执行,这只是退而求其次的解决方案。实际情况是:浏览器在执行setTimeout()和setInterval()时,会设置一个最小时间阈值,通常是4ms。vari=0var_start=+newDate()functionfn(){setTimeout(()=>{console.log("执行次数,时间",++i,+newDate()-_start)if(i===10){return}fn()},0)}fn()所以用MessageChannel执行宏任务,模拟setTimeout(fn,0),暂时还没有延时。实现如下//计算当前帧的结束时间点vardeadlineTime//保存任务varcallback//建立通信varchannel=newMessageChannel()varport1=channel.port1;varport2=channel.port2;//接收并执行宏任务port2.onmessage=()=>{//判断当前帧是否还空闲,即返回剩余时间consttimeRemaining=()=>deadlineTime-performance.now();const_timeRemain=timeRemaining();//有空闲时间,有回调任务if(_timeRemain>0&&callback){constdeadline={timeRemaining,//计算剩余时间didTimeout:_timeRemain<0//当前帧是否完成}//执行回调callback(deadline)}}window.requestIdleCallback=function(cb){requestAnimationFrame(rafTime=>{//结束时间点=开始时间点+一帧时间16.667msdeadlineTime=rafTime+16.667//保存任务callback=cb//发送宏任务port1.postMessage(null);})}4.React源码requestHostCallbackSchedulerHostConfig.js[8]执行宏任务(回调任务)requestHostCallback:触发一个宏任务performWorkUntilDeadline。performWorkUntilDeadline:宏任务处理。不管你有没有足够的时间,如果你有,就执行它。回调任务执行完毕后,是否有下一个回调任务,即判断hasMoreWork。如果有,继续执行port.postMessage(null);letscheduledHostCallback=null;letisMessageLoopRunning=false;constchannel=newMessageChannel();//port2发送constport=channel.port2;//port1接收channel.port1.onmessage=performWorkUntilDeadline;constperformWorkUntilDeadline=()=>{//有任务要executeif(scheduledHostCallback!==null){constcurrentTime=getCurrentTime();//yieldafter`yieldInterval`ms,regardlessofwhereweareinthevsync//cycle.Thismeansthere'salwaystimeremainingatthebeginningof//themessageevent.//计算一帧过期时间点deadline=currentTime+yieldInterval;consthasTimeRemaining=true;try{//执行回调后,判断后续是否还有其他任务scheduledHostCallback=null;}else{//Ifthere'smorework,schedulethenextmessageeventattheend//oftheprecedingone.//还有其他任务,推入下一个宏任务队列port.postMessage(null);}}catch(error){//ifaschedulertaskthrows,exitthecurrentbrowsertasksothe//errorcanbeobserved.port.postMessage(null);throwerror;}}else{isMessageLoopRunning=false;}//Yieldingtothebrowserwillgiveitachancetopaint,sowecan//resetthis.needsPaint=false;};//requestHostCallback一帧执行任务requestHostCallback=function(callback){//回调注册scheduledHostCallback=callback;if(!isMessageLoopRunning){isMessageLoopRunning=true;//进入宏任务队列port.postMessage(null);}};cancelHostCallback=function(){scheduledHostCallback=null;};参考[1]Elab掘金:ReactFiber架构分析:https://juejin.cn/post/7005880269827735566[2]window.requestIdleCallback():https://developer.mozilla.org/zh-CN/docs/Web/API/Window/requestIdleCallback[3]RequestIdleCallback实验:https://github.com/Linjiayu6/FE-RequestIdleCallback-demo[4]可能离题:requestIdleCallback每秒仅调用20次-我的6x2内核Linux机器上的Chrome,它对UI工作并不是很有用。requestAnimationFrame被更频繁地调用,但具体针对顾名思义的任务。:https://github.com/facebook/react/issues/13206#issuecomment-418923831[5]requestAnimationFrame:https://developer.mozilla.org/zh-CN/docs/Web/API/Window/requestAnimationFrame[6]DOMHighResTimeStamp:https://developer.mozilla.org/zh-CN/docs/Web/API/DOMHighResTimeStamp[7]MessageChannel():https://developer.mozilla.org/zh-CN/docs/Web/API/MessageChannel/MessageChannel[8]SchedulerHostConfig.js:https://github.com/facebook/react/blob/v17.0.1/packages/scheduler/src/forks/SchedulerHostConfig.default.js