重新学习React最近重新学习了React。由于这两年没用过React,突然重新学习了一些有趣的概念。首先是React的Scheduler(调度器)。因为我对React的概念还在React15之前(也就是那个没有hooks的时代),所以接触到Scheduler(调度器)感觉很有意思;在我的印象中,React的架构分为两层(React16之前Reconciler(协调器)——负责发现变化的组件)——负责将变化的组件渲染到页面中现在添加了Scheduler(调度器),那又怎样调度器的用途是什么?调度器的作用是调度任务的优先级。Reconciler会优先处理高质量的任务。为什么我们需要一个调度器?要理解为什么我们需要一个调度器,我们需要知道以下痛点;React什么时候更新;16如何更新之前的React;16之前React带来的痛点;首先说说React什么时候更新。众所周知,主流浏览器的刷新频率是60HZ,也就是说主流浏览器需要1000/60ms约等于16.666ms。然后我们需要知道当你打开一个页面时浏览器做了什么;总之,就是一张图。CSSOM树的构建时机和JS的执行时机是根据你解析的link标签和script标签来决定的。确认的;因为当React开始更新的时候,已经完成了一部分工作(开始回流和重绘),所以精简后可以归纳为以下几个步骤,上面的整个过程称为一帧,通俗地说就是16.6msjs(主流浏览器)的事件循环完成后会渲染页面;那么React什么时候更新页面呢?React会在执行完上面的整个过程后,在空闲时间内进行更新,所以如果执行完上面的过程需要10ms,React会在剩下的6.6ms(一般5ms左右)进行更新;在React16之前,组件的mount阶段会调用mountComponent,update阶段会调用updateComponent,我们知道react的更新是从外到内更新的,所以当时的方法是使用递归逐步更新子组件,而这个过程是不可中断的,所以当子组件嵌套层级太深时,会出现卡顿,因为这个过程是同步不可中断的,所以react16之前使用的是同步更新策略,这显然不符合React的快速响应理念;为了解决上述同步更新带来的痛点,React16使用异步可中断更新来代替它,所以在React16中引入了Scheduler(调度器)。调度程序如何工作?调度器主要包括两个功能。时间片优先级调度关于时间片很好理解。我们已经提到了Readt更新会在重绘和渲染后的空闲时间内执行;所以本质上和方法requestIdleCallback类似;requestIdleCallback(fn,timeout)方法常用于处理一些优先级比较低的任务,任务会在浏览器空闲时执行,它有两个致命缺陷,不适用于所有浏览器(兼容性)并引发不稳定。当浏览器FPS在20左右时,它会更流畅(与React的快速响应相反)。因此,React放弃了requestIdleCallback,实现了更多的功能。强大的requestIdleCallbackpolyfill是Scheduler。首先我们看一下JS在浏览器中的执行过程和requestIdleCallback的执行时机。Scheduler的时间片会以回调函数的形式在异步宏任务中执行;请查看源代码varschedulePerformWorkUntilDeadline;//If(typeoflocalSetImmediate==='function'){//Node.js和旧版IE。//我们更喜欢setImmediate的原因有几个。////与MessageChannel不同,它不会阻止Node.js进程退出。//(即使这是调度程序的DOM分支,您也可以在这里//混合使用Node.js15+,它具有MessageChannel和jsdom。)//https://github.com/facebook/react/issues/20756////而且,它运行得更早是我们想要的语义。//如果其他浏览器实现了它,最好使用它。//尽管这两者都不如原生调度。schedulePerformWorkUntilDeadline=function(){localSetImmediate(performWorkUntilDeadline);};}elseif(typeofMessageChannel!=='undefined'){//判断浏览器是否可以执行MessageChannel对象,也是一个异步宏任务,优先级高于setTimeout//DOM和Worker环境。//我们更喜欢MessageChannel因为4mssetTimeoutclamping.varchannel=newMessageChannel();varport=channel.port2;channel.port1.onmessage=performWorkUntilDeadline;schedulePerformWorkUntilDeadline=function(){port.postMessage(null);};}else{//如果当前非旧版IE和node环境没有MessageChannel,使用setTimeout执行回调函数//非浏览器环境只应回退到这里。schedulePerformWorkUntilDeadline=function(){localSetTimeout(performWorkUntilDeadline,0);};}可以看到Scheduler是使用三个Asynchronousmacrotask方式,老版本IE和node环境下使用setImmediate,一般使用MessageChannel如果当前环境不支持MessageChannel,则改用setTimeout。说完时间分片,再说调度优先级;首先我们要知道对应的五个优先级//立即超时varIMMEDIATE_PRIORITY_TIMEOUT=-1;//过期//最终超时varUSER_BLOCKING_PRIORITY_TIMEOUT=250;//将过期varNORMAL_PRIORITY_TIMEOUT=5000;//正常优先级任务varLOW_PRIORITY_TIMEOUT=10000;//lowprioritytask//NevertimesoutvarIDLE_PRIORITY_TIMEOUT=maxSigned/3过期时间越短的任务优先级越高。调度器根据任务的优先级来调度任务。它会优先安排具有较高优先级的任务,然后再安排具有较低优先级的任务。如果突然调度一个低优先级的任务插入一个高优先级的任务,会中断并保存任务让高优先级的任务跳入队列,等有空闲时间片后再取出队列执行;我们看主要入口函数unstable_scheduleCallbackfunctionunstable_scheduleCallback(priorityLevel,callback,options){varcurrentTime=exports.unstable_now();变种开始时间;//获取任务延迟if(typeofoptions==='object'&&options!==null){vardelay=options.delay;if(typeofdelay==='number'&&delay>0){//延迟任务startTime=currentTime+delay;}else{开始时间=当前时间;}}else{开始时间=当前时间;}变量超时;//根据不同的优先级(过期时间)给超时赋值switch(priorityLevel){caseImmediatePriority:timeout=IMMEDIATE_PRIORITY_TIMEOUT;休息;caseUserBlockingPriority:timeout=USER_BLOCKING_PRIORITY_TIMEOUT;休息;;休息;caseLowPriority:timeout=LOW_PRIORITY_TIMEOUT;休息;caseNormalPriority:default:timeout=NORMAL_PRIORITY_TIMEOUT;休息;}//计算任务延迟时间(执行)varexpirationTime=startTime+timeout;//新任务初始化varnewTask={id:taskIdCounter++,callback:callback,priorityLevel:priorityLevel,startTime:startTime,expirationTime:expirationTime,sortIndex:-1};//如果startTime大于currentTime,说明优先级低,是延迟任务if(startTime>currentTime){//这是延迟任务。//将startTime保存到新任务中,用于任务排序(执行顺序)newTask.sortIndex=startTime;//使用小顶堆将新任务插入延迟任务队列进行排序//当前startTime>currentTime所以当前任务是延迟任务,插入到延迟任务队列中push(timerQueue,newTask);//如果可执行任务队列为空或者新任务是延迟任务的第一个if(peek(taskQueue)===null&&newTask===peek(timerQueue)){//所有任务都被延迟是延迟最早的任务。if(isHostTimeoutScheduled){//取消现有超时。//取消延迟调度cancelHostTimeout();}else{isHostTimeoutScheduled=true;}//安排超时。requestHostTimeout(handleTimeout,startTime-currentTime);}}else{newTask.sortIndex=expirationTime;//推入可执行队列push(taskQueue,newTask);//等到下次我们屈服。//当前可调度的非跳转任务if(!isHostCallbackScheduled&&!isPerformingWork){isHostCallbackScheduled=true;requestHostCallback(flushWork);//Execute}}returnnewTask;}从代码中可以看出Scheduler中的任务为队列,分别以可执行队列taskQueue和延迟队列timerQueue的形式保存。当有新任务进入unstable_scheduleCallback方法时,会将任务放入延迟队列timerQueue中进行排序(优先级根据任务的sortIndex),如果延迟队列timerQueue中的一个任务变为可执行(currentTmie>startTime),我们会将该任务放入可执行队列taskQueue,取出最快到期的任务ExecutionSummaryReact原来的同步更新被异步可中断更新代替.实现异步可中断更新的关键是Scheduler。Scheduler的主要功能是时间分片和优先级调度。实现时间分片的关键是requestIdleCallbackpolyfill。调度任务是异步宏任务。实现优先级调度的关键是当前任务的过期时间。到期时间越短的优先级越高。根据任务的优先级,分别存放在可执行队列和延迟队列中;
