最近在重学React,因为近两年没应用React忽然重学发现一些很有意思的概念,首先便是React的Scheduler(调度器) 因为我对React的概念还停留在React 15之前(就是那个没有hooks的年代),所以接触Scheduler(调度器) 让我感觉很有意思;

在我印象中React的架构分为两层(React 16 之前)

  • Reconciler(协调器)—— 负责找出变动的组件
  • Renderer(渲染器)—— 负责将变动的组件渲染到页面上

现在减少了Scheduler(调度器) ,那么调度器有什么用?调度器的作用是调度工作的优先级,高优工作优先进入Reconciler

咱们为什么须要Scheduler(调度器)

要理解为什么须要Scheduler(调度器) 咱们须要晓得以下几个痛点;

  1. React在何时进行更新;
  2. 16之前的React怎么进行更新;
  3. 16之前的React带来的痛点;

首先咱们讲讲React何时进行更新,家喻户晓支流的浏览器的刷新频率是60HZ,也就是说支流的浏览器实现一次刷新须要1000/60 ms约等于16.666ms

而后咱们须要晓得浏览器在你开启一个页面的时候做了什么;总结下来就是一张图


CSSOM树的构建机会与JS的执行机会是根据你解析的link标签与script标签来确认的;因为当React开始更新时已实现局部工作(开始回流与重绘),所以通过精简,能够归为以下几个步骤

而以上的整个过程称之为一帧,艰深点讲就是在16.6ms之内(支流浏览器)js的事件循环进行实现之后会对页面进行渲染;那么React在何时对页面进行更新呢?react会在执行完以上整个过程之后的闲暇工夫进行更新,所以如果执行以上流程用了10ms则react会在余下的6.6ms内进行更新(个别5ms左右);

在React16之前组件的mount阶段会调用mountComponentupdate阶段会调用updateComponent,咱们晓得react的更新是从外向内进行更新,所以过后的做法是应用递归逐渐更新子组件,而这个过程是不可中断的,所以当子组件嵌套层级过深则会呈现卡顿,因为这个过程是同步不可中断的,所以react16之前采纳的是同步更新策略,这显然不合乎React的疾速响应理念;

为了解决以上同步更新所带来的痛点,React16采纳了异步可中断更新来代替它,所以在React16当中引入了Scheduler(调度器)

Scheduler如何进行工作

Scheduler次要蕴含两个作用

  1. 工夫切片
  2. 优先级调度

对于工夫切片很好了解,咱们曾经提到了Readt的更新会在重绘出现之后的闲暇工夫执行;所以在实质上与requestIdleCallback 这个办法很类似;

requestIdleCallback(fn,timeout)

这个办法罕用于解决一些优先级比拟低的工作,工作会在浏览器闲暇的时候执行而它有两个致命缺点

  • 不是所有浏览器实用(兼容性)
  • 触发不稳固,在浏览器FPS为20左右的时候会比拟晦涩(违反React疾速响应)

因而React放弃了requestIdleCallback 而实现了性能更加弱小的requestIdleCallback polyfill 也就是 Scheduler

首先咱们看下JS在浏览器中的执行流程与requestIdleCallback的执行机会

Scheduler的工夫切片将以回调函数的形式在异步宏工作当中执行;请看源码

var schedulePerformWorkUntilDeadline;//node与旧版IE中执行if (typeof localSetImmediate === 'function') {  // Node.js and old IE.  // There's a few reasons for why we prefer setImmediate.  //  // Unlike MessageChannel, it doesn't prevent a Node.js process from exiting.  // (Even though this is a DOM fork of the Scheduler, you could get here  // with a mix of Node.js 15+, which has a MessageChannel, and jsdom.)  // https://github.com/facebook/react/issues/20756  //  // But also, it runs earlier which is the semantic we want.  // If other browsers ever implement it, it's better to use it.  // Although both of these would be inferior to native scheduling.  schedulePerformWorkUntilDeadline = function () {    localSetImmediate(performWorkUntilDeadline);  };} else if (typeof MessageChannel !== 'undefined') {  //判断浏览器是否执行MessageChannel对象,同属异步宏工作,优先级高于setTimeout  // DOM and Worker environments.  // We prefer MessageChannel because of the 4ms setTimeout clamping.  var channel = new MessageChannel();  var port = channel.port2;  channel.port1.onmessage = performWorkUntilDeadline;  schedulePerformWorkUntilDeadline = function () {    port.postMessage(null);  };} else {  //如果以后非旧IE与node环境并且不具备MessageChannel则应用setTimeout执行回调函数  // We should only fallback here in non-browser environments.  schedulePerformWorkUntilDeadline = function () {    localSetTimeout(performWorkUntilDeadline, 0);  };}

能够看到Scheduler在应用了三种异步宏工作形式,在旧版IE与node环境中应用setImmediate,在个别状况下应用MessageChannel如果以后环境不反对MessageChannel则改用setTimeout

那么讲完工夫切片,咱们来讲讲调度优先级;首先咱们要晓得对应的五种优先级

// Times out immediatelyvar IMMEDIATE_PRIORITY_TIMEOUT = -1;//曾经过期// Eventually times outvar USER_BLOCKING_PRIORITY_TIMEOUT = 250;//将要过期var NORMAL_PRIORITY_TIMEOUT = 5000;//个别优先级工作var LOW_PRIORITY_TIMEOUT = 10000;//低优先级工作// Never times outvar IDLE_PRIORITY_TIMEOUT = maxSigned31BitInt;//最低优先级

能够看到过期时长越低的工作优先级越高,Scheduler是依据工作优先级状况来调度的,它会优先调度优先级高的工作,再调度优先级低的工作,如果在调度低优先级工作时忽然插入一个高优先级工作则会中断并保留该工作让高优先级工作插队,在之后有闲暇工夫片再从队列中取出执行;咱们来看主入口函数unstable_scheduleCallback

function unstable_scheduleCallback(priorityLevel, callback, options) {  var currentTime = exports.unstable_now();  var startTime;   //获取工作提早  if (typeof options === 'object' && options !== null) {    var delay = options.delay;    if (typeof delay === 'number' && delay > 0) {       //提早工作      startTime = currentTime + delay;    } else {      startTime = currentTime;    }  } else {    startTime = currentTime;  }  var timeout;  //依据不同优先级对应工夫给timeout赋值(过期工夫)  switch (priorityLevel) {    case ImmediatePriority:      timeout = IMMEDIATE_PRIORITY_TIMEOUT;      break;    case UserBlockingPriority:      timeout = USER_BLOCKING_PRIORITY_TIMEOUT;      break;    case IdlePriority:      timeout = IDLE_PRIORITY_TIMEOUT;      break;    case LowPriority:      timeout = LOW_PRIORITY_TIMEOUT;      break;    case NormalPriority:    default:      timeout = NORMAL_PRIORITY_TIMEOUT;      break;  }  //计算工作延迟时间(执行)  var expirationTime = startTime + timeout;  //新工作初始化  var newTask = {    id: taskIdCounter++,    callback: callback,    priorityLevel: priorityLevel,    startTime: startTime,    expirationTime: expirationTime,    sortIndex: -1  };   //如果startTime大于currentTime则阐明优先级低,为提早工作  if (startTime > currentTime) {    // This is a delayed task.    //将startTime存入新工作,用于工作排序(执行程序)    newTask.sortIndex = startTime;    //采纳小顶堆,将新工作插入提早工作队列进行排序    //以后startTime > currentTime所以当前任务为提早工作插入提早工作队列    push(timerQueue, newTask);    //若可执行工作队列为空或者新工作为提早工作的第一个    if (peek(taskQueue) === null && newTask === peek(timerQueue)) {      // All tasks are delayed, and this is the task with the earliest delay.            if (isHostTimeoutScheduled) {        // Cancel an existing timeout.        //勾销延时调度        cancelHostTimeout();      } else {        isHostTimeoutScheduled = true;      } // Schedule a timeout.      requestHostTimeout(handleTimeout, startTime - currentTime);    }  } else {    newTask.sortIndex = expirationTime;    //推入可执行队列    push(taskQueue, newTask);    // wait until the next time we yield.    //以后可调度无插队工作    if (!isHostCallbackScheduled && !isPerformingWork) {      isHostCallbackScheduled = true;      requestHostCallback(flushWork);//执行    }  }  return newTask;}

从代码中能够看到Scheduler中的工作以队列的模式进行保留别离是
可执行队列taskQueue提早队列timerQueue
当新工作进入办法unstable_scheduleCallback会将任放到提早队列timerQueue中进行排序(优先级按照工作的sortIndex),如果提早队列timerQueue中有工作变成可执行状态(currentTmie>startTime)则咱们会将工作放入咱们会将工作取出并放入可执行队列taskQueue并取出最快到期的工作执行

总结

React是以异步可中断的更新来代替原有的同步更新,而实现异步可中断更新的要害是SchedulerScheduler次要的性能是工夫切片优先级调度,实现工夫切片的要害是requestIdleCallback polyfill,调度工作为异步宏工作。而实现优先级调度的要害是当前任务到期工夫,到期工夫短的优先级更高,依据工作的优先级别离保留在可执行队列延时队列,;