React 源码系列:整体认识 React 的 Schduler 库

624 阅读5分钟

「这是我参与11月更文挑战的第22天,活动详情查看:2021最后一次更文挑战

看完了上一篇文章可能很多人会觉得很懵,依旧不能理解 scheduler 这个库能做什么以及为什么能做,因为我当时还没看完整个库的代码,缺少全局观。今天,趁着一点点时间,看完整个 Scheduler.js 的代码,来总结一下scheduler的核心概念。

全局对象的引用

上篇文章也说过了,类似 setTimeout 等全局对象(函数皆对象),react 不会直接使用它,而是用另一个别名变量去引用该对象,通过该别名使用对象,以防止用户的某些操作污染了全局变量导致这些对象失去原来的作用引起 react 的异常。在 Scheduler.js 取别名引用的总共有以下几种:

原始对象别名变量
performancelocalPerformance
DatelocalDate
setTimeoutlocalSetTimeout
clearTimeoutlocalClearTimeout
setImmediatelocalSetImmediate

全局变量

Scheduler 定义了一系列的全局变量,它们都有重要的语义,为了方便后面理解,这里列出关键的全局变量

  • isInputPending:判断是否正在处理输入事件,这是一个函数,本质是非标准 dom api ——navigator.scheduling.isInputPending
  • isHostTimeoutScheduled:是否执行了 requestHostTimeout 却还没开始执行 hostTimeout
  • isHostCallbackScheduled:是否执行了 requestHostCallback
  • isPerformingWork:是否正在处理 workLoop 逻辑
  • isSchedulerPaused:调试相关
  • isMessageLoopRunning:是否运行信息循环
  • scheduledHostCallback:已经被调度的任务回调
  • currentTask:当前要执行的任务
  • currentPriorityLevel:当前任务的优先级
  • enableIsInputPending:打包时如果配置了 enableNewReconciler 为 true,则该值也为 true,理解时代码我们视它恒为 true 就好。
  • enableIsInputPendingContinuous:同上
  • enableProfiling:测试性能的开关。用于允许测量整个react应用渲染的频率和代价。不是我们关心的逻辑,遇到有关代码跳过即可。
  • enableSchedulerDebugging:是否开启调试开关

优先级队列

Scheduler 用到了优先级队列,这是一个最小堆的数据结构,提供了 push、pop、peek 三个方法。其中 peek 是取最小堆的堆头,以对象的 sortIndex 字段作为比较依据。

全局有两个存储任务的优先级队列,分别为 taskQueue 和 timerQueue

任务结构

   var newTask = {
     id: taskIdCounter++,
     callback,
     priorityLevel,
     startTime,
     expirationTime, 
     sortIndex: -1,
   };

callback 包含了该任务的实际操作,callback被执行才意味着该任务被调度完成,可以说 Scheduler 的作用就是控制 callback 的执行时机。

id 是任务的唯一标识符,每次新建任务都令 taskIdCounter 自增,保证了不同任务一定不会有重复 id。

sortIndex 是任务在优先级队列的优先级,优先级队列是用一个最小堆实现的。

priorityLevel 也用来表示优先级,优先级越高,取值越小,有 ImmediatePriority、UserBlockingPriority、 NormalPriority、LowPriority、 IdlePriority 几种类型,对应number分别为1、2、3、4、5,

任务的过期时间 expirationTime 与它的 priorityLevel 有关,priorityLevel 的值越小,过期时间越早。对于 ImmediatePriority,过期时间等于开始时间减 1,意味着能够立即被调度

操作

unstable_scheduleCallback

  1. 根据传入的 callback 和 priorityLevel 创建任务
  2. 如果是延时任务,将任务放到 timerQueue 中,如果 taskQueue 队头是空的,且新任务排在 timerQueue的队头,则将 isHostTimeoutScheduled 设为 true,然后执行requestHostTimeout,调度一个 timeout,在这之前如果 isHostTimeoutScheduled 已经是 true,则执行 cancelHostTimeout 清除已经存在的 timeout。
  3. 如果不是延时任务,则放进任务队列中。如果 isPerformingWork 为 false 并且 isHostCallbackScheduled 也是 false,则将 isHostCallbackScheduled 设为 true,并且执行 requestHostCallback(flushWork)。

timeout相关

  • requestHostTimeout:通过 localSetTimeout 实现,设置定时器,经过指定时间后异步执行回调
  • cancelHostTimeout:通过 localClearTimeout 实现,清除 requestHostTimeout 设置的定时器

核心

  • advanceTimers:从 timerQueue 不断取队头任务,主要做两件事:1. 如果队头任务的 callback 是 null,则从队列中移除;2. 如果 callback 不为 null 的队头任务 startTime小于当前时间 ,则将任务从 timerQueue 移动到 taskQueue,其中 sortIndex要更新成 与 expirationTime 字段相同。重复循环直到没有 callback 为 null 且 startTime 小于当前时间的任务,也就是说,当 advanceTimers 调用结束后,timerQueue 高优先级任务到了该开始的时间就会进入 taskQueue
  • flushWork:如果有通过 requestHostTimeout 设置的定时器回调还没有被执行,则清除定时器,放弃该回调,然后会执行 workLoop。执行 workLoop 之前保存工作现场——保存 currentPriorityLevel, isPerformingWork = true。执行 workLoop 结束后恢复工作现场——将保存的值还原到全局变量 currentPriorityLevel。返回值是 workLoop 的返回值,表示是否还有任务没有被执行
  • workLoop:调用 advanceTimers,然后在一个while 循环进行某些操作。退出循环后判断 currentTask 是否为 null,如果为 null,说明任务队列已经没有可调度的内容了,从 timerQueue 取出一个队头任务firstTimer,任务可能还没到开始的时间 firstTimer.startTime,执行 requestHostTimeout ,实现当时间到了firstTimer.startTime 的时候,会异步执行 handleTimeout
  • handleTimeout:requestHostTimeout 的回调逻辑。全局变量 isHostTimeoutScheduled 设为 false,执行 advanceTimers。如果 isHostCallbackScheduled 为 true,则结束;否则如果 taskQueue 还有任务,则 isHostCallbackScheduled 设为 true,然后执行 requestHostCallback(flushWork)。如果 taskQueue 没有任务了,则从 timerQueue 找队头,如果不为 null, 则执行 requestHostTimeout,时间到了就再次执行 handleTimeout
  • requestHostCallback:将入参赋给全局变量 scheduledHostCallback,如果全局变量 isMessageLoopRunning 为 false,则重设为 true,并执行 schedulePerformWorkUntilDeadline
  • schedulePerformWorkUntilDeadline:尽快地异步执行 performWorkUntilDeadline。有三种异步策略,优先使用 localSetImmediate,如果引擎不支持 setImmediate 则使用 MessageChannel 实现异步,如果还是不行就用 localSetTimeout
  • performWorkUntilDeadline:如果全局变量 scheduledHostCallback不为 null,则执行它,得到返回值,表示是否还有任务需要被执行,如果为 true,继续调用 schedulePerformWorkUntilDeadline,否则重置 scheduledHostCallback 为 null,设 isMessageLoopRunning 为 false,结束

workLoop 的 while 循环

  1. 循环条件之一是 currentTask 不为空
  2. 如果任务的过期时间大于当前时间并且没有剩余时间或某些case需要立即打断loop了,则退出循环
  3. 更新 currentPriorityLevel 为 currentTask.priorityLevel
  4. 如果 currentTask 的 callback 是一个函数则执行它,然后调用 advanceTimers,否则从 taskQueue 移除队头。callback 返回值如果是函数,则将返回值赋值给 currentTask.callback(在执行callback之前, currentTask.callback 重置为 null)
  5. 如果 currentTask 等于 taskQueue的队头,则从 taskQueue移除队头
  6. 从taskQueue 取队头任务,赋值给currentTask