首页 > 其他分享 >教你如何实现react Scheduler(一)

教你如何实现react Scheduler(一)

时间:2022-09-18 03:44:30浏览次数:125  
标签:浏览器 常量 端口 react 如何 任务 循环 Scheduler btn

最近一直在看react源码,react搭建fiber树的逻辑还是比较容易理解的,但是说到任务调度相关的逻辑,就显得比较混乱了。参考了一些资料和react调度器的源码后,决定实现一个简单版的调度器。相信按照本文的思路,你就能理解为什么react需要一个调度器来调度任务。

简单的背景知识:

我们知道现在大部分设备的帧率都是60fps,也就是说浏览器每16.7ms就会绘制一次。如果页面上有一些动画,那么16.7s绘制一次,看起来更流畅。

我们先写一个简单的CSS动画:一个普通的div左右滑动

 <!DOCTYPE html >  
 < html lang = "en" >  
 <头>  
 <元字符集=“UTF-8”>  
 < meta http-equiv = "X-UA-Compatible" 内容 = "IE=edge" >  
 <元名称=“视口”内容=“宽度=设备宽度,初始比例=1.0”>  
 <title>文档</ title >  
 <风格>  
 #block { 宽度 : 50px ;高度:50px;边距:0 0;背景颜色:#ddd;动画:移动5s线性无限;位置:绝对; } @keyframes 移动 { 0% { 左 : 0 ; } 25% { 左:100px ; } 50% { 左:200px ; } 75% { 左 : 100px ; } 100% { 左:0 ; } }</ style >  
 </ head >  
 <身体>  
 < div id = "块" ></ div >  
 </ body >  
 </ html >  
 复制代码

使用 Google Chrome 的性能记录面板查看:

1663129187558.png

在主线程上,一帧的时间是16.7ms。让我们放大看看浏览器在一帧时间内做了什么:

1663136182178.png

要完成一个绘图,您需要执行 Schedule Style Recalculation、Recalculate Style、Layout、Pre-Paint、Paint、Composite Layers。这里我们不详细介绍浏览器在每个阶段做了什么,只注意渲染是在主线程上完成的,由 CPU 完成。通常浏览器会每 16.7ms 绘制一次,但如果在这个事件循环中有任务在执行,则需要等待任务完成才能进行绘制。如果任务耗时过长,就会减少抽奖次数,也就是所谓的“丢帧”。因为我们现在有一个非常简单的页面,没有 js 任务,浏览器每 16.7ms 绘制一次,动画看起来很流畅。

现在让我们添加一个按钮。点击后会创建5个任务。每个任务需要 20 毫秒,并且会立即执行。

 <身体>  
 <按钮ID =“btn”>点击我</ button >  
 </ body >  
 复制代码

绑定事件:

 常量作品 = [];  
 常量 btn = 文档。 getElementById('btn');  
 btn。 onclick = 函数 () {  
 for ( 让 i = 0; i < 5; i++) {  
 作品。推(宏任务)  
 }  
 冲洗工作();  
 }  
  
 函数宏任务(){  
 常量开始 = 新日期()。获取时间();  
 while ( new Date().getTime() - start < 20) {}  
 }  
  
 函数冲洗工作(){  
 而(作品。长度){  
 常量工作 = 工作。转移();  
 工作。调用(空);  
 }  
 }  
 复制代码

当你点击按钮的时候,你会发现滑动的div卡了一会儿。从下图中可以看出,在五个宏任务完成之前,浏览器是不会进行渲染的。在此期间,页面无法更新或响应用户操作。 .

1663137618473.png

如果有上千个或上百个任务需要点击一个按钮,那么浏览器就会长时间卡顿,这显然是不能接受的。最简单的改造方法是执行一个任务,将后续的任务处理放到下一个事件循环中,让浏览器在这个事件循环中进行绘制。精通浏览器原理的你一定知道可以使用setTimeout来实现:

 常量作品 = [];  
 常量 btn = 文档。 getElementById('btn');  
 btn。 onclick = 函数 () {  
 for ( 让 i = 0; i < 50; i++) {  
 作品。推(宏任务)  
 }  
 冲洗工作();  
 }  
  
 函数宏任务(){  
 常量开始 = 日期。现在();  
 while ( Date.now() - start < 20) {}  
 }  
  
 函数冲洗工作(){  
 工作循环();  
 }  
 函数工作循环(){  
 常量工作 = 工作。转移();  
 如果(工作){  
 工作。调用(空);  
 // 只执行一个任务,下一个事件循环会再次处理  
 设置超时(工作循环,0);  
 }  
 }  
 复制代码

打开控制台分析:

1663139245105.png

现在您可以看到,现在每个宏任务都没有链接在一起,它们是在不同的事件循环中执行的。每个任务完成后,浏览器都会进行一次绘图,即使有很多任务要执行,动画也不会卡顿。

但是仔细看,后面的宏任务之间的间隔似乎比较大,放大的时候间隔在4ms左右。我们当前一个任务的执行时间是20ms,超过了16.7ms。事实上,页面已经有点卡住了。主线程资源这么紧张,每个事件循环浪费4ms,绝对不能接受。很多人应该都听说过setTimeout的最小延迟限制,这大概就是说虽然你setTimeout是0秒,但实际上嵌套多层后,宏任务至少要4ms左右才会进入任务队列。

setTimeout 不再起作用了,还有其他选择吗?答案是肯定的,我们可以使用 MessageChannel 将任务放入宏任务队列。
MessageChannel的使用就不详细介绍了。简单来说,使用这个api,我们可以监听一个消息事件。当事件被触发时,事件处理程序将被添加到宏任务队列中。对应我们的例子,我们可以在绑定onmessage的时候执行workLoop,只执行workLoop中的一项任务。如果还有未执行的任务,则 postMessage 并在下一个事件循环中继续处理。

 常量频道 = 新的 MessageChannel();  
 常量端口 2 = 通道。端口2;  
 常量端口 1 = 通道。端口1;  
 端口 1。 onmessage = 工作循环;  
  
 常量作品 = [];  
 常量 btn = 文档。 getElementById('btn');  
 btn。 onclick = 函数 () {  
 for ( 让 i = 0; i < 50; i++) {  
 作品。推(宏任务)  
 }  
 冲洗工作();  
 }  
  
 函数宏任务(){  
 常量开始 = 日期。现在();  
 while ( Date.now() - start < 20) {}  
 }  
  
 函数冲洗工作(){  
 工作循环();  
 }  
 函数工作循环(){  
 常量工作 = 工作。转移();  
 如果(工作){  
 工作。调用(空);  
 端口 2。 postMessage(空);  
 }  
 }  
 复制代码

重新执行分析后,宏观任务基本没有差距:

1663141415359.png

目前我们最小的任务单元的执行时间是20ms。因为超过16.7ms会导致页面卡死,所以我们实际上应该保证单个任务不能超过16.7ms。假设设计合理,我们最小的任务单元执行时间不会超过 2ms(这里随机设置为 1ms 或 2ms)。然后让我们看看当你点击按钮并执行 1000 个任务时会发生什么。

 常量频道 = 新的 MessageChannel();  
 常量端口 2 = 通道。端口2;  
 常量端口 1 = 通道。端口1;  
 端口 1。 onmessage = 工作循环;  
  
 常量作品 = [];  
 常量 btn = 文档。 getElementById('btn');  
 btn。 onclick = 函数 () {  
 for ( 让 i = 0; i < 1000; i++) {  
 作品。推(宏任务)  
 }  
 冲洗工作();  
 }  
  
 函数宏任务(){  
 常量时间 = [ 1, 2];  
 const zeroOrOne = 数学。回合(数学。随机());  
 常量开始 = 日期。现在();  
 while ( Date.now() - start < time[zeroOrOne]) {}  
 }  
  
 函数冲洗工作(){  
 工作循环();  
 }  
 函数工作循环(){  
 常量工作 = 工作。转移();  
 如果(工作){  
 工作。调用(空);  
 端口 2。 postMessage(空);  
 }  
 }  
 复制代码

1663144692017.png

分析运行结果,我们可以看到浏览器绘制的帧率仍然不是60fps,而且我们的任务占用主线程的时间太长了。所以我们需要一种机制来在一帧中执行尽可能多的任务,同时为浏览器留出足够的时间来绘制页面并响应用户交互。

最终我们的设计方案是:在一个事件循环中,我们只占用主线程5ms,如果超过5ms,则将主线程的控制权交还给浏览器,在下一个事件中处理任务环形。

具体思路:

声明一个全局队列taskQueue来存储任务;

声明一个全局变量startTime来表示任务调度的开始时间。接收到onmessage事件时,获取当前时间并赋值给startTime,然后开始调度任务;

调度任务:从taskQueue队列中取出一个任务,获取当前时间currentTime,计算currentTime-startTime,如果大于等于5ms,则表示调度任务时长已经达到5ms,跳出循环,如果队列中还有任务,postMessage交给主线程控制,等待下一个事件循环调度任务。

浏览器绘制页面并响应用户交互后,在下一个事件循环中再次调度任务,重新计算 currentTime 和 startTime。这时候,它们之间的差值一定不能超过5ms,取出一个任务执行,然后更新currentTime。再次进入while循环判断currentTime-startTime是否大于5ms,如果大于5ms则交出控制权,否则继续执行下一个任务。

修改后的代码:

 常量频道 = 新的 MessageChannel();  
 常量端口 2 = 通道。端口2;  
 常量端口 1 = 通道。端口1;  
 端口 1。 onmessage = performWorkUntilDeadline;  
  
 常量任务队列 = [];  
 让开始时间 = - 1;  
 常量 frameYieldMs = 5; // 任务的连续执行时间不能超过5ms  
 让 currentTask = null; // 用于保存当前任务  
  
 btn。 onclick = 函数 () {  
 for ( 让 i = 0; i < 1000; i++) {  
 任务队列。推(宏任务)  
 }  
 // 在下一个事件循环中开始调度任务  
 端口 2。 postMessage(空);  
 }  
  
 功能 performWorkUntilDeadline() {  
 开始时间 = 性能。现在(); // 更新开始时间  
 让 hasMoreWork = true;  
 尝试 {  
 hasMoreWork = flushWork();  
 } 最后 {  
 当前任务=空;  
 如果(有更多工作){  
 端口 2。 postMessage(空);  
 }  
 }  
 }  
  
 函数冲洗工作(){  
 返回工作循环();  
 }  
  
 函数工作循环(){  
 // 使用 currentTask 全局变量来存储当前任务似乎有点难看。  
 // 其实就是实现以后的任务优先级和任务队列功能。我不管,就这样写吧。  
 当前任务=任务队列[0];  
 而(当前任务){  
 如果(应该YieldToHost()){  
 休息;  
 }  
 当前任务。调用(空);  
 任务队列。转移(); // 完成的任务从队列中移除  
 当前任务=任务队列[0]; // 继续下一个任务  
 }  
 如果(当前任务){  
 // 还有任务需要在下一个事件循环中处理  
 返回真;  
 }  
 }  
  
 函数应该YieldToHost() {  
 // 任务是否应该被挂起  
 const currentTime = 性能。现在();  
 如果(当前时间 - 开始时间 < frameYieldMs){  
 返回假;  
 }  
 返回真;  
 }  
  
 函数宏任务(){  
 常量时间 = [ 1, 2];  
 const zeroOrOne = 数学。回合(数学。随机());  
 常量开始 = 性能。现在();  
 while (performance.now() - start < time[zeroOrOne]) {}  
 }  
 复制代码

好吧,我们来看看运行结果:现在浏览器的帧率可以保持在60fps,效果已经很不错了。但是目前我们的任务队列只是一个普通的先进先出队列,并没有实现优先级和任务切割的功能。在下一篇文章中,我们将继续沿用 react 的实现思路,使用 min heap 来实现优先级队列。

1663316870570.png

版权声明:本文为博主原创文章,遵循CC 4.0 BY-SA版权协议。转载请附上原文出处链接和本声明。

这篇文章的链接: https://homecpp.art/3417/8663/0753

版权声明:本文为博主原创文章,遵循 CC 4.0 BY-SA 版权协议,转载请附上原文出处链接和本声明

本文链接:https://www.qanswer.top/37460/26531803

标签:浏览器,常量,端口,react,如何,任务,循环,Scheduler,btn
From: https://www.cnblogs.com/amboke/p/16704124.html

相关文章