scheduler模块

scheduler模块用于管理重绘完成后回调的执行逻辑。从输出分析,对整个调度过程进行梳理。

基础前提

浏览器渲染与事件循环

浏览器采用多进程架构,包含浏览器主进程、渲染进程、插件进程、GPU进程。每开启一个tab时浏览器就会开启一个渲染进程,该进程里包含多个线程:负责运行jsdomcss计算和页面渲染的主线程,运行worker的工作线程等。主线程解析html时,遇到script标签,会暂停html的解析,并开始加载、解析并执行js代码、为了调度事件、用户交互、渲染、网络请求这些操作,主线程会通过事件循环来处理。事件循环的过程为:

  • 同步任务

  • 一个宏任务

  • 清空微任务队列

  • 判断是否渲染视图(是否有重排、重绘、渲染间隔是否达到16.7ms等),为真则渲染视图,否则跳至步骤1,页面渲染前调用requestAnimationFrame回调函数,最后判断是否启动空闲时间算法,如果启动就调用requestIdleCallback

常见的宏任务:事件回调、xhr回调、定时器、I/OMessageChannel

常见的微任务:PromiseGeneratorAsync/AwaitMutationObserver

时间片

js在浏览器中的执行是单线程的,长时间的js任务执行可能会阻塞其他浏览器任务,如页面渲染、用户交互等,有可能会造成用户的卡顿感。schedule中采用时间分片的策略,将任务细化为不同的优先级,利用浏览器的空闲时间进行任务的执行保证UI操作的流畅。浏览器的调度API主要分为两种,高优先级的requestAnimationFrame与低优先级的requestIdleCallback

js任务分解到时间片中执行后,一次时间循环最多只执行一个时间片,若还有未完成的任务,将这些任务放到后面的事件循环的时间片中执行,保证不会阻塞其他的浏览器任务。

requestAnimationFrame

requestAnimationFrame传入一个回调函数作为参数,该回调函数会在浏览器下一次重绘之前执行。回调函数执行次数通常是每秒60次,在大多数遵循w3c建议的浏览器中,回调函数执行次数通常与浏览器屏幕刷新次数相匹配。大多数浏览器中,当 requestAnimationFrame运行在后台标签页或隐藏的iframe中时,requestAnimationFrame会被暂停调用。

requestAnimationFrame函数接收一个接收DOMHighResTimeStamp参数的callback函数作为参数,返回一个requestIdcancelAnimationFrame以取消。

requestIdleCallback

浏览器每秒一般60帧,帧与帧的间隔成为时间片,长度为 1000 / 6016ms,如果一帧渲染完成的时间小于16ms,这个时间片就有空闲时间。空闲时间会被执行requestIdleCallback的回调函数。当超过timeout时间还不执行callback时,callback将会被强制执行,造成的后果是,阻塞本地渲染,延长渲染时间,造成卡顿、延迟等。

callback函数接收IdleDeadline接口类型的参数,是一个对象,包含两个属性

  • didTimeout,布尔值,表示任务是否超时

  • timeRemaining,表示当前时间片剩余的时间。

requestIndleCallback会返回一个id,传入cancelIdleCallback可结束对应的回调。

使用时间片的实现:

scheduler每执行一次performWorkUntilDeadline函数表示执行了一个时间片,执行该函数前会通过MessageChannelsetTimeout将该函数放入宏任务队列,优先使用MessageChannel

每执行一个时间片时,将时间片长度 yieldInterval和过期时间deadline放入执行的任务中,并通返回值判断是否存在未执行完的任务(表示时间片已用完)。若存在为执行完的任务,则让任务在下次事件循环继续执行。

export {
 ImmediatePriority as unstable_ImmediatePriority, // 1
 UserBlockingPriority as unstable_UserBlockingPriority, // 2
 NormalPriority as unstable_NormalPriority, // 3
 IdlePriority as unstable_IdlePriority, // 5
 LowPriority as unstable_LowPriority, // 4
 unstable_runWithPriority,
 unstable_next,
 unstable_scheduleCallback,
 unstable_cancelCallback,
 unstable_wrapCallback,
 unstable_getCurrentPriorityLevel,
 shouldYieldToHost as unstable_shouldYield,
 unstable_requestPaint,
 unstable_continueExecution,
 unstable_pauseExecution,
 unstable_getFirstCallbackNode,
 getCurrentTime as unstable_now,
 forceFrameRate as unstable_forceFrameRate,
};
export const unstable_Profiling = enableProfiling
 ? {
     startLoggingProfilingEvents,
     stopLoggingProfilingEvents,
   }
 : null;

输出函数分析

任务优先级

react内对任务优先级的定义。Scheduler中任务有不同的优先级,每个优先级有对应的过期时间,在生成任务时根据优先级和创建时间生成任务的过期时间,任务过期后才会放入taskQueue执行,否则放入timerQueue等待执行。

优先级 含义 过期时间 过期时间的值
NoPriority 0 无优先级
ImmediatePriority 1 最高优先级 IMMEDIATE_PRIORITY_TIMEOUT -1
UserBlockingPriority 2 用户阻塞型优先级 USER_BLOCKING_PRIORITY_TIMEOUT 250
NormalPriority 3 普通优先级 NORMAL_PRIORITY_TIMEOUT 5000
LowPriority 4 低优先级 LOW_PRIORITY_TIMEOUT 10000
IdlePriority 5 空闲优先级 IDLE_PRIORITY_TIMEOUT maxSigned31BitInt = Math.pow(2, 30) - 1

环境中设置变量分析


//任务存储在小顶堆上
var taskQueue = [] // 任务队列
var timerQueue = []; // 延时任务队列
var taskIdCounter = 1; // 递增id计数器, 用于维护插入顺序
var isSchedulerPaused = false; // 暂停调度程序,用于调试
var currentTask = null; // 当前任务
var currentPriorityLevel = NormalPriority; //3 当前执行任务的优先级
var isPerformingWork = false; // 是否正在执行任务,在执行工作时设置的, 以防止重新进入
var isHostCallbackScheduled = false; // 是否有主任务正在执行,是否调度了 taskQueue, isHostCallbackScheduled为true后才把时间片放到宏任务队列,之后开始执行任务
var isHostTimeoutScheduled = false; // 是否有延时任务正在执行,是否调度了 timerQueue, 设置了 timeout回调

SchedulerHostConfig

let requestHostCallback; // 请求回调
let cancelHostCallback; // 取消回调
let requestHostTimeout; // 请求超时
let cancelHostTimeout; // 取消超时
let shouldYieldToHost;
let requestPaint; // 请求绘制
let getCurrentTime; // 获取当前时间, 优先用 performance.now(), 或者用 Date.now() - 初始时间
let forceFrameRate; // 强制帧率
  • 如果Scheduler运行在非DOM环境中,使用setTimeout回退到一个简单的实现。环境中检测不到window或者不支持MessageChannel时:

    • requestHostCallback

    • cancelHostCallback

    • requestHostTimeout

    • cancelHostTimeout

    • shouldYieldToHost

    • requestPaint

    • forceFrameRate

unstable_runWithPriority

unstable_runWithPriority(*priorityLevel*, *eventHandler*)主要逻辑:

  • currentPriorityLevel设置为priorityLevel,然后执行eventHandler

  • 最后将currentPriorityLevel改回之前的值

unstable_next

unstable_next(*eventHandler*)主要逻辑:

  • currentPriorityLevel高于NormalPriority情况下设置为NormalPriority,否则保持当前优先级

  • 执行eventHandler

  • 最后将currentPriorityLevel改回之前的值

unstable_scheduleCallback

整体流程:

  • scheduler中任务有不同优先级,每个优先级有对应的过期时间,在生成任务根据优先级和创建事件生成任务的过期时间,任务过期后才会放入taskQueue执行,否则放入timerQueue等待执行

  • TaskQueueTimerQueue是两个用小顶堆实现的具有优先级的任务队列。TaskQueue中优先级的索引是expirationTimeTimerQueue中优先级的索引使用的是startTimeSchedulerMinHeap.js中实现了对小顶堆的peekpoppush方法。使用advanceTimers方法可以依据指定的时间和任务的开始时间将TimerQueue中的任务更新到TaskQueue中,更新时会同时更新索引使用的值为expirationTime

  • 通过unstable_scheduleCallback添加任务,生成一个任务对象。任务对象包含任务id、任务执行函数、优先级、开始时间、过期时间和在队列中的顺序。任务通过传入参数中的delay属性值来判断该任务是同步任务还是异步任务。

  • 若生成的任务是同步任务,则将该任务推入taskQueue

    • 如果当前taskQueue是未被调度且任务未被执行,则使用requestHostCallback调用flushWork方法

    • requestHostCallback方法内会触发message事件,performWorkUntilDeadline函数作为message事件的回调将推入事件循环的宏任务队列

    • performWorkUntilDeadline方法会执行一个时间片的任务,时间片用完后会判断是否还有未执行的任务,如果有则再次触发message事件

    • flushWork先取消timerQueue的回调,之后设置isPerformingWorkfalse,并调用workLoop方法执行taskQueue中的任务

    • workLoop会不断取出taskQueue中的任务,直到执行完所有的任务或者执行完所有超时的任务且时间片已用完。最后若存在未执行完的任务,则返回true,否则重新设置timerQueue中的回调,并返回false

  • 若生成的任务是异步任务,则将任务推入timerQueue。如果当前taskQueue为空且新任务在timerQueue中优先级最高,使用requestHostTimeout调度handleTimeout方法

  • handleTimeout首先判断当前是否在调度taskQueue,若没有在调度,则判断taskQueue是否为空,如果不为空,则调度taskQueue,否则调度timerQueue

Untitled Diagram.drawio.png

unstable_scheduleCallback(*priorityLevel*, *callback*, *options*)主要逻辑为,根据输入返回newTask

  • 根据传入的 options更新startTime,根据传入的priorityLevel更新 timeout,然后计算expireTime,定义newTask
var newTask = {
  id: taskIdCounter++,
  callback,
  priorityLevel,
  startTime,
  expirationTime,
  sortIndex: -1,
};
  • 对于延时任务,startTime > currentTimestartTime设置为sortIndex,将任务添加到timerQueue队列,如果taskQueue为空且timerQueue只有newTask一个延时任务。是否有延时任务正在执行,如果有,清除定时器,否则将isHostTimeoutScheduled设置为true。执行requestHostTimeout,延时处理handleTimeout

    • requestHostTimeout逻辑:接收callbackms,经过ms后执行callback,将getCurrentTime()的值传入.

    • handleTimeout逻辑:isHostTimeoutScheduled设置为false,执行advanceTimers。如果 isHostCallbackScheduledfalse,即没有主任务正在执行,设置isHostCallbackScheduledtrue,将flushWork传递给requestHostCallback

    • advanceTimers逻辑:接收一个参数currentTime,检查timerQueue中的任务,将不再延时的任务添加到taskQueue中。将timerQueue中的堆顶任务弹出,如果不存在timer.callback,任务取消,并且弹出timerQueue,如果timer.startTime <= currentTime,任务弹出timerQueue,并且添加到taskQueue中.

    • flushWork逻辑:接收(hasTimeRemaining, initialTime)两个参数,isHostCallbackScheduled设置为false,如果isHostTimeoutScheduled,设置为false,取消定时器,然后执行workLoop(hasTimeRemaining, initialTime)

    • workLoop逻辑:接收(hasTimeRemaining, initialTime)两个参数,当前任务不为空或任务不停止的情况下,执行循环。当当前任务还没有过期,但是到了deadline,则跳出循环;currentTask有回调的情况下,执行回调,currentTask等于栈顶元素的情况下,将任务从taskQueue中弹出,执行advanceTimers将不再延时的任务添加到任务队列;currentTask没有回调的情况下,将任务从taskQueue中弹出。currentTask为空的情况下,获取timerQueue的栈顶任务,放入requestHostTimeout中。

https://someu.github.io/2020-11-10/react-scheduler%E6%BA%90%E7%A0%81%E8%A7%A3%E6%9E%90/

https://juejin.cn/post/6889314677528985614

https://juejin.cn/post/6914089940649246734

md格式的文件直接粘过来有点丑,待补充

推荐阅读更多精彩内容