这篇讨论 Fiber 架构调度部分的实现原理,你将看到:
- Fiber 架构的调度能力的分层设计。
- Scheduler 的分片原理,以及调度器如何基于浏览器能力实现“空闲回调”和“时间管理”。
- Scheduler 中的任务是怎样注册管理、派发执行、定义优先级的。
- React 如何利用 Scheduler 实现“可中断更新”。
Part 0 背景
Fiber 架构是 React 一次伟大的革新。
React Fiber 是 React 核心算法的重新实现。 它的主要特点是渐进式渲染: 能够将渲染工作分割成块,并将其分散到多个帧。 其他关键特性包括在新的更新到来时暂停、中止或重用工作的能力; 为不同类型的更新分配优先级的能力; 以及新的并发方式。 ——GitHub - acdlite/react-fiber-architecture: A description of React’s new core algorithm, React Fiber
为了实现上述特性,Fiber 架构加入了关键的 Scheduler(调度器)。有了 Scheduler 这个“大脑”,React 在 setState 后不再直接启动“协调”过程,而是把本次更新注册到 Scheduler,再由 Scheduler 根据浏览器剩余空闲时间、优先级等因素派发给 Reconciler(协调器),并通过中断查询控制协调的中断重启。(协调就是我们说的包含 Diffing 的虚拟 DOM 构建计算过程,参考上篇)
![](https://i-blog.csdnimg.cn/blog_migrate/7ee97aa933f3c3791e1f0a6d0db56442.png)
编辑切换为全宽
添加图片注释,不超过 140 字(可选)
所以这样的 Scheduler 要支持哪些能力呢?
1.要能维护一个“任务池”
2.要提供一系列“优先级”的定义,并派发高优任务
3.要能感知浏览器的空闲,并根据剩余时间,随时给出“能不能继续工作”的建议
而 Reconciler 也要通过对 Scheduler 能力的调用,管理协调过程的发起、暂停、终止。
Part 1 调度的分层实现和调用链路
为了进一步看清 Scheduler 在 React 中的角色,这里有一张图解释整个调度的分层实现和调用链路。
![](https://i-blog.csdnimg.cn/blog_migrate/d904e57ad15fccea37445e5576105776.png)
编辑切换为居中
添加图片注释,不超过 140 字(可选)
React 源码是分包组织的,packages 下面有若干个相对独立的包,react-reconciler、scheduler 就是其中两个,包下面是具体文件模块,这里列举了四个关键的。
- 当我们调用 setState 之类的 api,组件会把状态入队后调 ReactFiberScheduler 注册本次修改。
- ReactFiberScheduler,主要做 Fiber 调度、协调管理这些事。比如注册调度并提供回调函数发起协调、管理当前协调的节点。这一层并不直接依赖 Scheduler 的 API(可能觉得不优雅?),而是 react-reconciler 内部的一个封装模块。
- SchedulerWithReactIntegration,就是那个封装模块,直接调 Scheduler,并把API改了改名字透传出来。
- Scheduler,是调度器最核心的实现,实现了Part0 中说的第1、2点能力。这里做了优先级定义、任务池维护/注册/取消、任务调度执行、中断判断。这些能力又依赖对宿主时间片的判断,什么时候空闲、剩多少时间。在早些版本的 Fiber 中宿主时间片也做在 Scheduler,后来拆出去了。
- SchedulerHostConfig,实现了宿主时间片部分,也就是 Part0 的第 3 点能力。
接下来我们按依赖顺序,自底向上,看看各层具体的实现方式。
Part 2 SchedulerHostConfig 宿主时间片判断
SchedulerHostConfig 要基于宿主(这里只谈浏览器)API,实现时间片管理。它要回答两个问题:
1.浏览器什么时候有空?空了叫我
2.此时此刻,我要不要让出线程给浏览器?
为什么要回答这些问题,怎么回答这些问题,就要从浏览器机制说起。
单线程JS 和浏览器帧
众所周知,浏览器里 JS 是单线程的。不但 JS 执行本身单线程,而且 JS 执行引擎和浏览器渲染引擎都挤在单个线程里。
好在大部分情况下,JS 执行、浏览器渲染都足够快,所以浏览器只要在单位时间内交替执行 JS 引擎和渲染引擎就好。这个单位时间叫做“帧(frame)”,目前主流的是60fps,每秒60轮,快到用户肉眼根本看不出来交替执行。一帧的生命周期如下:
![](https://i-blog.csdnimg.cn/blog_migrate/7115deb5f37c01fb7f2f29b4c023e0b8.png)
编辑切换为居中
添加图片注释,不超过 140 字(可选)
\
但不排除某一帧下,触发了一个巨复杂的 js 逻辑,把当前帧事件耗完了还没跑完,甚至跨了几个帧都没跑完。那浏览器就拿他没办法,必须等他跑完才能做渲染,这样用户就发现:“哎,刚刚有段时间页面卡住不动了”。巧的是,一个很庞大的虚拟DOM树的 Diffing 就可能是这种卡帧的逻辑,所以必须在需要的时候“暂时”退出来,让浏览器先把这帧的渲染跑了,回头再继续跑。
再回到大多数情况。当 js 不那么复杂时,这一帧的 js 和渲染跑完后,是有剩余时间的。这时候浏览器就会通过某种方式通知出来。让我们知道:“浏览器现在有空闲了”。
浏览器的空闲回调
这时候大名鼎鼎的 requestIdleCallback 出场了。
window.requestIdleCallback()方法插入一个函数,这个函数将在浏览器空闲时期被调用。这使开发者能够在主事件循环上执行后台和低优先级工作,而不会影响延迟关键事件,如动画和输入响应。 —— requestIdleCallback - Web API 接口参考 | MDN
看起来完美对不对,但它的兼容性堪忧:
![](https://i-blog.csdnimg.cn/blog_migrate/e82b0256bd073f325f8be921349b2acc.png)
编辑切换为居中
添加图片注释,不超过 140 字(可选)
\
所以 React 找了个替代品 —— MessageChannel。
Channel Messaging API 的MessageChannel 接口允许我们创建一个新的消息通道,并通过它的两个 MessagePort 属性发送数据。 —— MessageChannel - Web API 接口参考 | MDN
这是个 Full Support 的 API。