最近在重学 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,调度工作为 异步宏工作 。而实现优先级调度的要害是当前任务到期工夫,到期工夫短的优先级更高,依据工作的优先级别离保留在 可执行队列 与延时队列,;