最近在重学React,由于近两年没使用React突然重学发现一些很有意思的概念,首先便是React的Scheduler(调度器) 由于我对React的概念还停留在React 15之前(就是那个没有hooks的年代),所以接触Scheduler(调度器) 让我感觉很有意思;
在我印象中React的架构分为两层(React 16 之前)
- Reconciler(协调器)—— 负责找出变化的组件
- Renderer(渲染器)—— 负责将变化的组件渲染到页面上
如今增加了Scheduler(调度器) ,那么调度器有什么用?调度器的作用是调度任务的优先级,高优任务优先进入Reconciler
我们为什么需要Scheduler(调度器)
要了解为什么需要Scheduler(调度器) 我们需要知道以下几个痛点;
- React在何时进行更新;
- 16之前的React怎样进行更新;
- 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阶段会调用mountComponent,update阶段会调用updateComponent,我们知道react的更新是从外向内进行更新,所以当时的做法是使用递归逐步更新子组件,而这个过程是不可中断的,所以当子组件嵌套层级过深则会出现卡顿,因为这个过程是同步不可中断的,所以react16之前采用的是同步更新策略,这显然不符合React的快速响应理念;
为了解决以上同步更新所带来的痛点,React16采用了异步可中断更新来替代它,所以在React16当中引入了Scheduler(调度器)
Scheduler如何进行工作
Scheduler主要包含两个作用
- 时间切片
- 优先级调度
关于时间切片很好理解,我们已经提到了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 immediately
var IMMEDIATE_PRIORITY_TIMEOUT = -1;//已经过期
// Eventually times out
var USER_BLOCKING_PRIORITY_TIMEOUT = 250;//将要过期
var NORMAL_PRIORITY_TIMEOUT = 5000;//一般优先级任务
var LOW_PRIORITY_TIMEOUT = 10000;//低优先级任务
// Never times out
var 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是以异步可中断的更新来替代原有的同步更新,而实现异步可中断更新的关键是Scheduler,Scheduler主要的功能是时间切片与优先级调度,实现时间切片的关键是requestIdleCallback polyfill,调度任务为异步宏任务。而实现优先级调度的关键是当前任务到期时间,到期时间短的优先级更高,根据任务的优先级分别保存在可执行队列与延时队列,;