Scheduler
先来整体的看一下调度系统是如何工作的:
调度系统的工作机制基本如图中所示:
-
我们在数据更新的同时,会注册一个 “渲染任务” 到调度系统中
-
调度系统将 “渲染任务” 加入到 “任务队列” 中进行排序(实际上是个最小堆),等待执行
-
同时,调度系统会注册 “调度任务” 到
**macrotask**
中 -
“调度任务” 执行时,会按顺序执行 “任务队列” 中的任务,直到为空或者本次 “调度任务” 的时间片到时。
-
如果时间片到时,而 “任务队列” 依然存在为执行的任务,需要再次注册 “调度任务” 到
**macrotask**
中
任务“队列”
React 是通过 **scheduleCallback**
接口来向调度系统中来注册任务。其中 **priority**
为本次任务的优先级,**callback**
为等待执行的任务。
这样的数据在调度系统内部会被转化成任务 **Task**
来进行保存。其中值得一说的是,**priority**
优先级在这里会被映射为超时时间 **timeout**
,即该任务在多久内必须要被执行。
可以看到图中 Task 记录的是任务的过期时间 **expirationTime**
,其实 **expirationTime = now() + timeout**
。
最终任务被添加到任务“队列”(其实是个堆结构)中,并按照任务的过期时间 **expirationTime**
进行排序,这样就可以每次从 “队列” 中取出最早要过期的任务先执行。
调度任务的工作
调度任务是指:将任务“队列”中的任务拿出来,依次执行的工作。具体的话,大家可以看 Scheduler 模块中的 **flushWork**
函数。
如上图所示,当有任务被添加到调度系统中时,调度系统不是立刻去执行 “调度任务”,而是将 “调度任务” 添加到 **macrotask**
中,等待执行。
这样一来,就不会因为任务执行太久,而阻塞同步代码的执行了。
其实,这也就变相地帮助 React 实现了 Automatic Batching 。
调度任务呢其实就是遍历任务“队列”去依次执行,如果所有任务都执行完成,那么调度任务自然可以结束。
除此之外呢,调度任务还有另外一种停止机制:时间切片。
时间切片
什么是时间切片呢?其实每次调度任务的执行都有限额的时间(比如 5 毫秒),当执行超过这个时间的时候,调度系统就需要先停下,注册下次的调度任务到 **macrotask**
。
这意味着,这个时候调度任务交出了JS线程的 “控制权” ,我们可以去处理交互产生的回调、页面渲染等事情了。
我们再引用一下《Concurrent 的奥秘》中的例子,它们的执行情况如下图:
上图这个是没有进行时间切片的同步渲染过程。可以看到,React 的 “渲染” 过程花了 50 毫秒才完成。在此期间,我们无法做任何事情,页面卡在了呢个地方,直到 JS 执行结束。
这个则是使用了时间切片的并发渲染过程,可以看到,对于一些耗时的 “渲染” 过程,React 将它 “拆分” 成了很多个 5 毫秒的小任务。
每个小任务执行完,都会把 “控制权” 交还,这时浏览器可以查看是否有交互回调需要执行,或者可以让浏览器渲染一帧画面。这样就使得我们页面始终保持了响应能力,不会卡住。
对于调度任务来说,它会在每处理完一个任务后,检查本次执行是否超时。如果超时就停下来,注册下一次的任务,如此循环往复… 时间切片就是这样在调度任务中实际应用。