type
status
date
slug
summary
tags
category
icon
password
Edited
Sep 23, 2024 04:41 PM
Created
Sep 19, 2024 02:36 PM
对于
React Scheduler
,它通过将任务切片并异步执行,避免了阻塞浏览器的主线程。很多人其实都看到过类似的文章了,甚至说去手写调度器,都写的很不错,所以本文将从一个新的角度探讨
React Scheduler
,揭示它是如何利用几个简单的 API 实现这一壮举的。React Scheduler
解析
首先,让我们回顾一下浏览器的任务类型:宏任务和微任务,以及它们是如何在事件循环中执行的。
在浏览器的一次事件循环中,包括宏任务、微任务和页面渲染三个部分。如果我们假设屏幕的刷新率是 60FPS,那么大约每 16ms 就会执行一次事件循环。
这 16ms 内我们还要留些事件给浏览器处理页面渲染。而剩余的时间我们就需要尽可能的充分利用。
那么我们怎么知道还剩多少空余时间呢,可以使用
requestIdleCallback
方法,它会插入一个函数,并在浏览器空闲的时候执行,同时传入一个参数包含剩下的空余时间。但是实际上并没有去使用
requestIdleCallback
去处理,为什么没有使用 requestIdleCallback
去实现呢?- 兼容性问题
虽然大部分主流浏览器都支持该方法,但是 Safari 等部分浏览器却仍不支持。
- 不确定性
requestIdleCallback
只会在浏览器空闲的时候去调用,这意味着它的执行有很大的不确定性,有可能会有较高的延迟。这对于一些高响应的任务,是没办法处理的。- 一致性
requestIdleCallback
属于浏览器 API,脱离了浏览器之后没办法使用。有了这个时间,我们就可以去执行空余时间这么长的任务了,但是我们怎么去确定我们的任务去执行多长时间呢?
时间分片
当然,这个我们肯定确认不了,这里我们就需要提到 React Scheduler 的一个精髓:时间分片。
它将大任务分割成无数个小任务,并在每次执行时判断是否还有空余时间,如果有,则继续执行;如果没有,则在下一次事件循环中继续。
而任务的细分属于代码层面的内容,实现的方式没有限制,但宏观上就是分成了一个个小任务。简单示例一下:
拆分完成之后,我们就可以实现上面的时间分片了,但这里还有一个问题,这次事件循环里的时间被充分利用了,但是怎么到下一个事件循环还能继续执行呢?
再回头看一下事件循环的执行顺序,宏任务 → 微任务 → 页面渲染,那么我们就需要在宏任务中插入一条任务来保持分片任务能继续往下执行,这里就需要用到
MessageChannel
了。至于为什么使用
MessageChannel
可以看看这篇文章:利用
MessageChannel
实现宏任务的插入,我们就能给 时间分片 实现一个闭环,我们来看看吧。功能上就是把之前理论的东西做了实践,包括空余时间的使用,宏任务的调度…
这里通过
performance.now()
来实现空余时间的执行,而没有使用 requestIdleCallback
,原因后面会解释。到这里,其实核心的知识都了解的差不多了,主要就是
- 任务分片
- 宏任务调度(保证能持续执行)
- 充分利用浏览器空余时间
最终实现了
React Scheduler
的时间分片效果。React Scheduler
精简版 —— Solid
最近在研究 Solid 的过程中,看到了 Solid 对于 React Scheduler 的引用,并稍作修改;并没有实现像 lane 车道等一下内容,只是实现了一个最精简的版本。
同时 Solid 也在
transition
等延迟处理的一下内容中用到了这一块内容。接下来,我们来看一个关于 React Scheduler 精简版的实现。
基本原理和上面的实现差不多,只是多了些细节的把控。
分析
Solid 的实现与上述原理基本一致,但增加了一些细节控制。以下是初始化函数
setupScheduler
的实现( 对应 init
):前半部分的原理和之前的基本一致,利用
MessageChannel
去调度宏任务,这里的任务执行是通过 scheduledCallback
去处理的。后半部分的话,是处理
shouldYieldToHost
方法,用于判断是否需要让出主线程。Scheduling
这里 Solid 进行了特殊处理,利用
navigator.scheduling
去获取浏览器的调度信息再进行处理,更好的利用时间去处理任务,但 navigator.scheduling
目前还属于实验性 API。isInputPending
尤其是里面的
isInputPending
方法,可以看一下 MDN 的介绍:Scheduling
接口的isInputPending
方法允许您检查事件队列中是否有待处理的输入事件,这表明用户正在尝试与页面交互。 如果您要运行任务队列,并且您希望定期让位于主线程以允许用户交互,以便应用程序尽可能保持响应式和高性能,则此功能非常有用。isInputPending
允许你只在有 input 待处理时让步,而不必以任意间隔进行。
有了此方法,可以最大限度去调度任务,同时在观察到用户对页面进行交互后把线程交还给浏览器。
接下来,我看看
scheduledCallback
的实现,也就是任务的执行逻辑(对应 while
):里面主要是对状态做了一些处理,主要看
workLoop
函数。接下来,我们看看任务是怎么加入队列的:
requestCallback
整体过程就是创建任务 → 加入任务队列 → 调度任务。
最后来看看
enqueue
里对任务做了什么处理。可以看出来,任务队列整体是按照过期时间进行排序的。
通过二分的方式,找到合适的位置进行插入即可。
其实整体看下来也没什么,和之前给的那个案例的原理是完全一致的,只是多了细节的处理。
只要了解了原理,其他这些额外的内容都是很好理解的。
最后贴一下 Solid 实现的 React Scheduler 的精简版的完整代码:
- 作者:JinSo
- 链接:https://jinso365.top/article/106cee95-0faf-8024-b4d9-cef5935e9f79
- 声明:本文采用 CC BY-NC-SA 4.0 许可协议,转载请注明出处。