React调度原理(scheduler)
- 在React运行时中,调度中心(位于scheduler包)
- 是整个React运行时的中枢(其实是心脏),所以理解了scheduler调度,就基本掌握了React的核心
- React两大循环:从宏观的角度介绍 React 体系中两个重要的循环,其中任务调度循环就是本文的主角
- Reconciler 运行流程从宏观的角度描述了 react-reconciler 包的核心作用
- 并把 reconciler 分为了4个阶段
- 其中第 2 个阶段注册调度任务串联了 scheduler 包和 react-reconciler 包
- 其实就是任务调度循环中的一个任务(task)
- 优先级管理
- React体系中的3中优先级的管理
- 着重源码中react-reconciler与scheduler包中关于优先级的转换思路
- 其中 SchedulerPriority 控制任务调度循环中循环的顺序
调度实现
调度中心最核心的代码,在 SchedulerHostConfig.default.js 中
内核
- 该js文件一共导出了8个函数,最核心的逻辑,就集中在了这8个函数中
export let requestHostCallback; // 请求及时回调: port.postMessage export let cancelHostCallback; // 取消及时回调: scheduledHostCallback = null export let requestHostTimeout; // 请求延时回调: setTimeout export let cancelHostTimeout; // 取消延时回调: cancelTimeout export let shouldYieldToHost; // 是否让出主线程 (currentTime >- deadline && needsPaint): export let requestPaint; // 请求绘:设置 needsPaint = true export let getCurrentTime; // 获取当前间 export let forceFrameRate; // 强制设置 yieldIntervol (让出主线程的周期)
- react可以在nodejs环境中使用,所以在不同的js执行环境中,这些函数的实现会有区别
- 下面基于普通浏览器环境,对这8个函数逐一分析
1 )调度相关:请求或取消调度
-
requestHostCallback
-
cancelHostCallback
-
requestHostTimeout
-
cancelHostTimeout
-
这4个函数源码很简洁,非常好理解,它们的目的就是请求执行(或取消)回调函数
-
现在重点介绍其中的及时回调 (延时回调的2个函数暂时属于保留api,17.0.2版本其实没有用上)
// MessageChannel const performWorkUntilDeadline=() => { //...省略无关代码 if (scheduledHostCallback !== null) { const currentTime = getCurrentTime(); // 更新 deadline deadline = currentTime + yieldInterval; // 执行 callback scheduledHostCallback(hasTimeRemaining, currentTime); } else { isMessageLoopRunning = false; }; }; const channel = new MessageChannel(); const port = channel.port2; channel.port1.onmessage = performWorkUntilDeadline; // 请求回调 requestHostCallback = function(callback) { // 1.保存callback scheduledHostCallback = callback; if(!isMessageLoopRunning) { isMessageLoopRunning= true; // 2.通过 MessageChannel消息 port.postMessage(null); } }; // 取消回调 cancelHostCallback = function() { scheduledHostCallback = null; }
-
很明显,请求回调之后 scheduledHostCallback = callback
-
然后通过MessageChannel发消息的方式触发performWorkUntilDeadline函数
-
最后执行回调 scheduledHostCallback
-
此处需要注意
- MessageChannel在浏览器事件循环中属于宏任务
- 所以调度中心永远是异步执行回调函数
2 )时间切片(timeslicing)相关:
-
执行时间分割,让出主线程
-
把控制权归还浏览器,浏览器可以处理用户输入,UI绘制等紧急任务
-
getCurrentTime
: 获取当前时间 -
shouldYieldToHost
: 是否让出主线程 -
requestPaint
: 请求绘制 -
forceFrameRate
: 强制设置yieldInterval(从源码中的引用来看,算一个保留函数,其他地方没有用到)const localPerformance = performance; // 获取当前时间 getCurrentTime = () => localPerformance.now(); // 时间切片周期,默认是5ms(如果一个task运行超过该周期,下一个task执行之前,会把控制权归还浏览器) let yieldInterval = 5; let deadline = 0; const maxYieldInterval = 300; let needsPaint = false; const scheduling = navigator.scheduling; // 是否让出主线程 shouldYieldToHost = function() { const currentTime = getCurrentTime(); if (currentTime >= deadline) { if (needsPaint || scheduling.isInputPending()) { // There is either a pending paint or a pending input. return true; } // There's no pending input. Only yield if we've reached the max // yield interval. return currentTime >= maxYieldInterval; // 在持续运行的react应用中, currentTime肯定大于300ms } else { // There's still time left in the frame. return false; } }; // 请求绘制 requestPaint = function() { needsPaint = true; }; // 设置时间切片的周期 forceFrameRate = function(fps) { if (fps < 0 || fps > 125) { // Using console['error'] to evade Babel and ESLint console['error']( 'forceFrameRate takes a positive int between 0 and 125, ' + 'forcing frame rates higher than 125 fps is not supported', ); return; } if (fps > 0) { yieldInterval = Math.floor(1000 / fps); } else { // reset the framerate yieldInterval = 5; }; }
-
这4个函数代码都很简洁,其功能在注释中都有解释.
-
注意 shouldYieldToHost 的判定条件:
- currentTime >= deadline: 只有时间超过 deadline 之后才会让出主线程
- 其中 deadline = currentTime + yieldInterval
- yieldInterval 默认是5ms, 只能通过 forceFrameRate 函数来修改
- 如果一个task运行时间超过5ms,下一个task执行之前,会把控制权归还浏览器
-
navigator.scheduling.isInputPending():
- 这facebook官方贡献给 Chromium 的api,现在已经列入W3C标准
- 用于判断是否有输入事件(包括:input框输入事件,点击事件等).
-
看完这8个内部函数,最后浏览一下完整的 performWorkUntilDeadline 回调的实现
const performWorkUntilDeadline = () => { if(scheduledHostCallback !== null) { const currentTime = getCurrentTime(); //1.获取当前时间 deadline = currentTime + yieldInterval; //2. 置deadline 当前时间 + 5ms, 也就是5ms超时 const hasTimeRemaining = true; try { // 3.执行回调,返回是否有还有剩余任务 const hasMoreWork = scheduledHostCallback(hasTimeRemaining, currentTime); if (!hasMoreWork) { // 没有剩余任务,退出 isMessageLoopRunning = false; scheduledHostCallback = null; } else { port.postMessage(null); // 有剩余任务,发起新的调度 } } catch (error) { port.postMessage(null); // 如有异常,重新发起调度 throw error; } } else { isMessageLoopRunning = false; } needsPaint = false; // 重置开关 }
-
核心总结,如下图
- MessageChanel 里面是宏任务异步执行
- 在执行的过程中,基于 performWorkUnitlDeadline
- 这个方法在执行任务过程中,有一个变量 hasMoreWork
- 基于这个变量判断是否还有任务,如果还有,则继续往里面注册回调,没有则退出