react Scheduler 实现示例教程

 更新时间:2022年09月17日 10:21:30   作者:fangjiapeng  
这篇文章主要为大家介绍了react Scheduler 实现示例教程,有需要的朋友可以借鉴参考下,希望能够有所帮助,祝大家多多进步,早日升职加薪

正文

最近在看react源码,react构建fiber树这一块逻辑还比较好理解,但是一旦涉及到任务调度相关的逻辑,看起来是一头雾水。在参考了一些资料和react scheduler源码后,我决定来实现一个简单版的scheduler,相信跟着本文的思路实现一遍,就可以理解为什么react需要有scheduler这个东西来调度任务。

简单的背景知识:

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

简单的css动画

先来写一个简单的css动画:一个普通的div左右滑动

<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <meta http-equiv="X-UA-Compatible" content="IE=edge">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <title>Document</title>
    <style>
        #block {
            width: 50px;
            height: 50px;
            margin: 0 0;
            background-color: #ddd;
            animation: move 5s linear infinite;
            position: absolute;
        }
        @keyframes move {
            0% {
                left: 0;
            }
            25% {
                left: 100px;
            }
            50% {
                left: 200px;
            }
            75% {
                left: 100px;
            }
            100% {
                left: 0;
            }
        }
    </style>
</head>
<body>
    <div id="block"></div>
</body>
</html>

使用谷歌浏览器的性能录制面板可以看到:

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

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

现在我们来加上一个按钮,点击之后会创建5个任务,每个任务耗时20ms,并且马上执行。

<body>
    <button id="btn">click me</button>
</body>

绑定事件:

const works = [];
const btn = document.getElementById('btn');
btn.onclick = function () {
    for (let i = 0; i < 5; i++) {
        works.push(macroTask)
    }
    flushWork();
}
function macroTask(){
    const start = new Date().getTime();
    while (new Date().getTime() - start < 20) {}
}
function flushWork(){
    while(works.length){
        const work = works.shift();
        work.call(null);
    }
}

点击按钮会发现,正在滑动的div卡顿了一下,通过下图可以看到,浏览器直到5个宏任务完成后才会执行渲染,在这段时间里面,页面不能更新,也不能响应用户操作。

etTimeout来实现

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

const works = [];
const btn = document.getElementById('btn');
btn.onclick = function () {
    for (let i = 0; i < 50; i++) {
        works.push(macroTask)
    }
    flushWork();
}
function macroTask(){
    const start = Date.now();
    while (Date.now() - start < 20) {}
}
function flushWork(){
    workLoop();
}
function workLoop(){
    const work = works.shift();
    if(work){
        work.call(null);
        // 只执行一个任务,后面的下个事件循环再处理
        setTimeout(workLoop, 0);
    }
}

打开控制台分析一下:

现在可以看到,现在每个宏任务都没有连在一起,它们在不同的事件循环里执行。每个任务完成后,浏览器都会执行一次绘制,就算要执行的任务非常多,动画也不会卡住不动了。

但是,仔细观察一下,后面的宏任务间隔好像都比较大,放大看间隔大概是4ms左右。我们现在一个任务的执行时间是20ms,超过了16.7ms,事实上页面已经有一点卡顿了。主线程资源这么紧张,每个事件循环居然还要浪费4ms,这肯定是不能接受的。很多人应该都听说过setTimeout的最小延时限制,大概意思就是虽然你是setTimeout零秒,实际上嵌套多层之后,至少要过4ms左右,宏任务才会进入到任务队列。

循环处理

setTimeout不能用了,有其他替代方案吗?答案是有的,我们可以使用MessageChannel来把任务放到宏任务队列。 MessageChannel的用法就不详细介绍了,简单地说,就是利用这个api,我们可以监听一个message事件,当事件触发的时候,事件处理函数这个任务会加入到宏任务队列。对应我们的例子,我们就可以绑定onmessage的时候执行workLoop, 在workLoop里面只执行一个任务,如果还有任务没有执行,那就postMessage,在下一个事件循环继续处理。

const channel = new MessageChannel();
const port2 = channel.port2;
const port1 = channel.port1;
port1.onmessage = workLoop;
const works = [];
const btn = document.getElementById('btn');
btn.onclick = function () {
    for (let i = 0; i < 50; i++) {
        works.push(macroTask)
    }
    flushWork();
}
function macroTask(){
    const start = Date.now();
    while (Date.now() - start < 20) {}
}
function flushWork(){
    workLoop();
}
function workLoop(){
    const work = works.shift();
    if(work){
        work.call(null);
        port2.postMessage(null);
    }
}

重新执行后再分析一下,宏任务之间基本没有间隔了:

目前我们的最小任务单元的执行时间是20ms。因为超过了16.7ms会导致页面变卡顿,所以实际上我们应该确保单个任务不能超过16.7ms。假设经过合理的设计,我们的最小任务单元执行时间不会超过2ms(这里随机设置成1ms或2ms)。然后再来看看点击按钮后执行1000个任务会怎么样。

const channel = new MessageChannel();
const port2 = channel.port2;
const port1 = channel.port1;
port1.onmessage = workLoop;
const works = [];
const btn = document.getElementById('btn');
btn.onclick = function () {
    for (let i = 0; i < 1000; i++) {
        works.push(macroTask)
    }
    flushWork();
}
function macroTask(){
    const time = [1, 2]; 
    const zeroOrOne = Math.round(Math.random());
    const start = Date.now();
    while (Date.now() - start < time[zeroOrOne]) {}
}
function flushWork(){
    workLoop();
}
function workLoop(){
    const work = works.shift();
    if(work){
        work.call(null);
        port2.postMessage(null);
    }
}

分析运行结果,可以看到现在浏览器绘制的帧率还是没有60fps,我们的任务占据主线程时间太长了。所以我们需要一种机制,使得在一帧的时间内尽可能执行多个任务,而且留有充足的时间给浏览器绘制页面和响应用户交互。

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

具体思路

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

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

调度任务:从taskQueue队列中取出一个任务,获取当前时间currentTime, 计算currentTime - startTime,如果大于或等于5ms,说明调度任务时长已经达到5ms了,break出循环,如果队列里还有任务,postMessage交出主线程控制权,等下个事件循环再调度任务。

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

改造后的代码:

const channel = new MessageChannel();
const port2 = channel.port2;
const port1 = channel.port1;
port1.onmessage = performWorkUntilDeadline;
const taskQueue = [];
let startTime = -1;
const frameYieldMs = 5; // 任务的连续执行时间不能超过5ms
let currentTask = null; // 用来保存当前的任务
btn.onclick = function () {
    for (let i = 0; i < 1000; i++) {
        taskQueue.push(macroTask)
    }
    // 在下个事件循环开始调度任务
    port2.postMessage(null);
}
function performWorkUntilDeadline() {
    startTime = performance.now(); // 更新开始时间
    let hasMoreWork = true;
    try {
        hasMoreWork = flushWork();
    } finally {
        currentTask = null;
        if(hasMoreWork) {
            port2.postMessage(null);
        }
    }
}
function flushWork(){
    return workLoop();
}
function workLoop() {
    // 这里用currentTask全局变量来保存当前任务看起来似乎有点丑。
    // 其实是为了后续实现任务优先级和任务插队功能,先不管,就这么写。
    currentTask = taskQueue[0];
    while(currentTask) {
        if(shouldYieldToHost()) {
            break;
        }
        currentTask.call(null);
        taskQueue.shift(); // 执行完的任务从队列中删除
        currentTask = taskQueue[0]; // 继续拿下一个任务
    }
    if(currentTask) {
        // 还有任务需要在下个事件循环处理
        return true;
    }
}
function shouldYieldToHost() {
    // 是否应该挂起任务
    const currentTime = performance.now();
    if(currentTime - startTime < frameYieldMs) {
        return false;
    }
    return true;
}
function macroTask(){
    const time = [1, 2]; 
    const zeroOrOne = Math.round(Math.random());
    const start = performance.now();
    while (performance.now() - start < time[zeroOrOne]) {}
}

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

以上就是react Scheduler 实现示例教程的详细内容,更多关于react Scheduler 教程的资料请关注脚本之家其它相关文章!

相关文章

  • React 保留和重置State

    React 保留和重置State

    这篇文章主要为大家介绍了React 保留和重置State实现示例详解,有需要的朋友可以借鉴参考下,希望能够有所帮助,祝大家多多进步,早日升职加薪
    2022-09-09
  • webpack 2的react开发配置实例代码

    webpack 2的react开发配置实例代码

    本篇文章主要介绍了webpack 2的react开发配置实例代码,小编觉得挺不错的,现在分享给大家,也给大家做个参考。一起跟随小编过来看看吧
    2017-07-07
  • React onBlur回调中使用document.activeElement返回body完美解决方案

    React onBlur回调中使用document.activeElement返回body完美解决方案

    这篇文章主要介绍了React onBlur回调中使用document.activeElement返回body完美解决方案,本文给大家介绍的非常详细,对大家的学习或工作具有一定的参考借鉴价值,需要的朋友可以参考下
    2023-04-04
  • 关于React动态修改元素样式的三种方式

    关于React动态修改元素样式的三种方式

    这篇文章主要介绍了关于React动态修改元素样式的三种方式,具有很好的参考价值,希望对大家有所帮助。如有错误或未考虑完全的地方,望不吝赐教
    2022-08-08
  • react echarts刷新不显示问题的解决方法

    react echarts刷新不显示问题的解决方法

    最近在写项目的时候遇到了一个问题,当编辑完代码后echarts图正常展示 , 可再次刷新页面 , echarts会消失,所以本文给大家介绍了react echarts刷新不显示问题原因和解决方法,需要的朋友可以参考下
    2024-02-02
  • React Native Modal 的封装与使用实例详解

    React Native Modal 的封装与使用实例详解

    这篇文章主要介绍了React Native Modal 的封装与使用,本文通过实例代码图文相结合给大家介绍的非常详细,对大家的学习或工作具有一定的参考借鉴价值,需要的朋友可以参考下
    2022-09-09
  • React-three-fiber使用初体验

    React-three-fiber使用初体验

    这篇文章主要为大家介绍了React-three-fiber的使用初体验,有需要的朋友可以借鉴参考下,希望能够有所帮助,祝大家多多进步,早日升职加薪
    2023-05-05
  • React Store及store持久化的使用教程

    React Store及store持久化的使用教程

    这篇文章主要介绍了React Store及store持久化的使用教程,本文给大家介绍的非常详细,对大家的学习或工作具有一定的参考借鉴价值,需要的朋友参考下吧
    2024-01-01
  • React使用PropTypes实现类型检查功能

    React使用PropTypes实现类型检查功能

    这篇文章主要介绍了React高级指引中使用PropTypes实现类型检查功能的方法,文中通过示例代码介绍的非常详细,对大家的学习或者工作具有一定的参考学习价值,需要的朋友们下面随着小编来一起学习吧
    2023-02-02
  • 在React中写一个Animation组件为组件进入和离开加上动画/过度效果

    在React中写一个Animation组件为组件进入和离开加上动画/过度效果

    这篇文章主要介绍了在React中写一个Animation组件为组件进入和离开加上动画/过度效果,本文通过实例代码给大家介绍的非常详细,具有一定的参考借鉴价值,需要的朋友可以参考下
    2019-06-06

最新评论