主要理解React Scheduler的 逻辑/设计 思路,了解Scheduler的作用是什么,如何运行的?
React源码版本:v16
作用
React引入的Fiber概念,将React的所有操作都化为Fiber"任务",而所有"任务"何时执行,则靠Scheduler来确定。
运行路径
此模块主要为Scheduler的逻辑思路,不涉及理解Scheduler主要功能的case不会过多赘述,也可以理解为这是一个"字典",带着你 "阅读" Scheduler的源码。 学会工具、思想,然后将其应用到工作中的各种场景。
-
初始化:当组件的状态发生变更时,会通过
scheduleWork
"初始化" Scheduler,准备开始新的一轮工作。不同组件的发起初始化位置不一,但基本都在数据更新完,准备开始渲染的阶段(Hooks组件在dispatchAction最后,ClassComponents在classComponentUpdater的setState之后)。 -
开始工作:
scheduleWork
内先判断当前的状态,假如没有在工作isWorking
,则通过调用requestWork
开始新的一轮工作,从 root 节点开始。 -
任务调度:
Scheduler
的主要任务是在浏览器空闲时执行任务,这里就涉及到两个点,任务封装、循环调度、空闲判定- 在
scheduleCallbackWithExpirationTime
中,会通过scheduleDeferredCallback
来封装Fiber的更新计算:performAsyncWork
为调度器中的任务。 - 而
scheduleDeferredCallback
可以看到是来自于scheduler/src/Scheduler.js
的unstable_scheduleCallback
:根据任务的优先级计算失效时间expirationTime
并封装成一个任务节点taskNode
,插入到当前的任务链表TaskQueue
里,通过ensureHostCallbackIsScheduled
启动整个TaskQueue
的工作。 taskNode
的内容:参照上面可以看到就是performAsyncWork
,而performAsyncWork
会通过workLoop
不断循环,不断处理单个Fiber节点为nextUnitOfWork
,当js繁忙则断开workLoop
,并将未完成的Fiber更新计算任务封装成新的callback更新任务丢给TaskQueue
。而恢复执行状态则是scheduler
会在下次js不繁忙时,重新触发TaskQueue
的执行。 所以需要额外了解的是,Scheduler
的终端,一般指的js繁忙则断开workLoop
,繁忙判定nextUnitOfWork !== null && !shouldYieldToRenderer()
- 如上,可以看到主要判定是否停下
workLoop
主要靠shouldYieldToRenderer
,而shouldYieldToRenderer
主要原理,则是判断帧是否已经超时。超时了,则认为js繁忙并停下执行中的任务链。
- 在
[重点] 循环调度机制
这里对 Scheduler
的循环调度再额外展开讲讲,这才是Scheduler
的重点
以前React是通过requestIdleCallback
来实现,现在可以看到React是基于MessageChannel
+rAF
来实现自行实现的。
为何需要requestIdleCallback
或MessageChannel
?
- 在浏览器繁忙断开了
workLoop
后,需要将主线程还给浏览器(这就是不使用微任务的原因)。 - 在下次浏览器空闲时,重新恢复任务执行。这俩api都是开启一个宏任务,这就保证了下一次浏览器空闲时一定会执行。
那为何弃用requestIdleCallback
?
- 兼容性问题:大部分浏览器都得到几乎很新的版本才会有该api
- 极端情况下不够用:因为
requestIdleCallback
的帧率上限问题,空闲期帧率会定为20fps,这不够React要求的最低30fps(w3c.github.io/requestidle...
为何不使用setTimeout
来开启宏任务?
setTimeout
只作为React的备用方案,即没有MessageChannel
的时候会使用setTimeout代替,原因便是轮询setTimeout
时timeout会延迟3~4ms,估计是因为浏览器实现上的问题,并没有找到相关的规范说明。
为何还有rAF
,它做了什么?
rAF
会注册animationTick
------用于更新帧时间的回调,并不涉及到任务的执行或恢复。animationTick
会计算出当前帧执行后的页面时间戳frameDeadline = rafTime + activeFrameTime;
这便是用于判定在React规定的帧率内,当前任务执行后是否超时,超时则需要先暂时中断任务执行,等待页面
总结
总结下Scheduler
调度机制的实现思路:
- 恢复执行:通过
MessageChannel
注册宏任务,恢复TaskQueue
执行。 - 停止执行:通过
rAF
计算当前帧率判断是否超时,假如当前帧已超时frameDeadline - currentTime <= 0
,则didTimout=true
,停止后续任务。 - 链式执行:
ensureHostCallbackIsScheduled
开启链式执行,将flusWork
这个不断出栈TaskQueue
的函数包给requestHostCallback
,而requestHostCallback
就是上面1、2点的恢复/停止任务的回调内容。