React调度流程详解(又是看了不后悔系列)

9,476 阅读7分钟

最近看了卡颂大佬公开讲的React调度流程讲解也跟着学习了一下然后自己进行了总结今天把代码更新到这里,喜欢研究React的原理的千万不要错过满满的干货

准备

今天会从零到一整体走一遍调度流程在源码中调度流程代码多到上千行,但是今天的实现大概一百多行就可以,我们先搞一些准备工作需要下载一些后面用到的包创造一个开发环境

1、创建一个文件目录,注意初始化一下package.json直接使用npm init -y

image.png

2、把用到的包下载一下可以使用yarn或者npm都可以

"react": "^18.1.0",
"react-dom": "^18.1.0",
"react-scripts": "^5.0.1",
"scheduler": "^0.22.0"

3、简单介绍一下我们的包reactreact-dom就不用讲了用过的都很熟悉,主要介绍一下react-scriptsscheduler

  • react-scripts相当于是直接使用cra启动了一个项目但是配置是都隐藏起来的可以基于这个配置进行重写整体看起来会比较简洁,今天我们只是简单启一个项目所以怎么配置就不介绍了

  • scheduler是Facebook团队开源的一个库主要用来时间调度使用的,在React框架中的调度核心用的也是这个库,里面抛出了一些优先级状态之后在下面我们会用到

github.com/facebook/re…

image.png

大概意思就是计划以后开源,并且React内部现在也在使用,但是API包还不文档但是也可以用

开始

准备工作完成之后我们开始写代码先看一下图上调度的思路

image.png

1、第一步将我们的任务插入到队列中等待调度执行
2、第二步取出work任务开始进行调度
3、第三步调度完成之后调用perform开始执行协调渲染

ok讲了这么多,我们接下来我们开始写代码了解一下其中具体的过程

1、创建index.html文件放两个容器标签在body中,后期我们会往里面添加内容

   <div id="root"></div>
   <div id="content"></div>

2、在这先把style.css样式直接贴出来了大家要是写的话直接复制进去就好这个不重要

*{
    margin: 0;
    padding: 0;
}
#content span {
    display: inline-block;
    width: 40px;
    height: 20px;
    text-align: center;
    line-height: 20px;
    margin-right: 5px;
    margin-top: 10px;
}

#content .span1{
    border: 1px solid red;
    color: red;
}
#content .span2{
    border: 1px solid blue;
    color: blue;
}
#content .span3{
    border: 1px solid green;
    color: green;
}
#content .span4{
    border: 1px solid #000;
    color: #000;
}
#content .span5{
    border: 1px solid purple;
    color: purple;
}

#root button {
    margin: 10px;
}

3、在package.json中配置启动命令

  "scripts": {
    "start": "react-scripts start"
  }

在index.ts中写我们的核心逻辑

1、先把样式和调度核心api都引进来下面会用到

import './style.css'
import {
    unstable_IdlePriority as IdlePriority, //空闲优先级
    unstable_LowPriority as LowPriority, //低优先级
    unstable_UserBlockingPriority as UseBlockingPriority, //用户阻塞优先级
    unstable_NormalPriority as NormalPriority, //正常优先级
    unstable_ImmediatePriority as ImmediatePriority, //立刻执行的优先级
    unstable_scheduleCallback as scheduleCallback, //调度器
    unstable_cancelCallback as cancelCallback, //取消调度
    unstable_shouldYield as shouldYield, //当前帧是否用尽了
    unstable_getFirstCallbackNode as getFirstCallbackNode, //返回当前第一个正在调度的任务
    CallbackNode
} from 'scheduler'

console.log(IdlePriority, '-------IdlePriority-----空闲优先级')
console.log(LowPriority, '--------LowPriority----低优先级')
console.log(NormalPriority, '-----NormalPriority-------正常优先级')
console.log(UseBlockingPriority, '----UseBlockingPriority--------用户阻塞优先级')
console.log(ImmediatePriority, '------ImmediatePriority------立刻执行的优先级')

源码中定义的优先级顺序来自于scheduler内部

image.png

2、定义类型方便下面使用

interface Work {
    count: number,
    priority: Priority
}

type Priority = typeof IdlePriority | typeof LowPriority | typeof UseBlockingPriority | typeof NormalPriority | typeof ImmediatePriority

3、我们会使用一个抽象概念作为组件渲染

//类似于这种代表渲染10个组件,这个得理解一下不然后面可能有点懵
const work:Work = {
    count: 10,
    priority: IdlePriority
}

4、我们创建几个执行按钮后面用来渲染不同优先级任务

const priority2UseList: Priority[] = [
    ImmediatePriority,
    UseBlockingPriority,
    NormalPriority,
    LowPriority,

]

const priority2Name = [
    'noop',
    'ImmediatePriority',
    'UseBlockingPriority',
    'NormalPriority',
    'LowPriority',
    'IdlePriority'
]

const rootDOM = document.querySelector('#root')
const contentDOM = document.querySelector('#content')

priority2UseList.forEach(priority => {
    const btnDOM = document.createElement('button')

    btnDOM.innerText = priority2Name[priority] + priority
    rootDOM.appendChild(btnDOM)

    btnDOM.onclick = function () {
        //要更新的组件次数
        const newWork = {
            count: 100,
            priority
        }


        workList.push(newWork)

        //开始执行调度过程
        schduler()
    }
})

按钮上的数字对应的不同优先级按钮

image.png

创建schduler方法,该方法的解释都在下面的注释中,如果那里不理解可以评论区讨论

//全局创建当前被调度的回调函数
let curCallback: CallbackNode | null = null  

//本次调度任务进行时,正在执行的调度的优先级默认是空闲优先级
let prevPriority: Priority = IdlePriority

//开始任务调度
function schduler() {
    //step1 获取当前调度的任务
    const cbNode = getFirstCallbackNode()

    //step2 获取优先级最高的任务
    const currWork = workList.sort((w1, w2) => w1.priority - w2.priority)[0]

    if (!currWork) {
        //没有任务了,考虑边界情况没
        curCallback = null
        cbNode && cancelCallback(cbNode)
        return
    }

    const { priority: curPriority } = currWork
    
    //step3 如果最新任务的优先级和当前执行的任务优先级一样就没必要打断当前执行的
    if (prevPriority === curPriority) {
        return
    }
    
    //step4 到了这个位置说明有更高优先级的任务我们需要中断掉当前的任务
    cbNode && cancelCallback(cbNode)

    //step5 开始任务调度执行
    curCallback = scheduleCallback(curPriority, perform.bind(null, currWork))
}

创建perform方法,这里其实就是源码中协调的开始

function perform(work: Work, didTimeout?: boolean) {

    //1、didTimeout 用来返回当前正在执行的任务是否需要中断掉(需要中断的时候说明有更高优先级的任务来了)

    //2、是否需要同步执行(同步执行的任务是不可中断的,也是优先级最高的)
    const needSync = work.priority === ImmediatePriority || didTimeout

    //3、shouldYield()获取浏览器是否还有剩余时间,每个调度任务过程只有5ms 如果超过5ms,就会返回true终止本次调度
    
    //step1 任务开始执行
    while ((needSync || !shouldYield()) && work.count) {
        work.count--
        //step2 开始协调任务执行一系列渲染计算
        insertItem(work.priority + '')
    }

    //step3 获取当前任务的优先级下次在进行schduler调度的时候可以用来和新任务比较
    prevPriority = work.priority
    
    //step4 到这个位置有两种情况
    //1.第一种 work.count执行完了也就是说当前组件执行完了
    //2.第二种 是调度器让我中断当前任务
        
    //step5 当前work执行完了删除队列中的任务并且将当前时间重置为空闲时间,方便下次进行调度计算
    if (!work.count) {
        const workIndex = workList.indexOf(work)
        workList.splice(workIndex, 1)
        prevPriority = IdlePriority
    }

    //step6 这下面的逻辑可能理解起来比较复杂
    
    //存储当前回调
    const prevCallback = curCallback
    //继续调度
    schduler()
    //获取新的回调
    const newCallback = curCallback
    
    //step7 符合这条逻辑说明新的回调函数和当前的回调函数一样,所以就没必要走schduler,直接走perform继续渲染就可以
    if (newCallback && (prevCallback === newCallback)) {
        return perform.bind(null, work)
    }
}

创建insertItem渲染组件

//插入
const insertItem = (content: string) => {
    const ele = document.createElement('span')
    ele.innerText = `${content}`
    ele.className = 'span' + content
    //加个阻塞可以看的渲染过程更清楚一些,不然一下子就渲染出来看不到效果
    doSomeBuzyWork(10000000)
    contentDOM.appendChild(ele)
}

//阻塞方法不用解释
const doSomeBuzyWork = (len: number) => {
    let result = 0;
    while (len--) {
        result += len;
    }
};

总结

可以看一下最后的结果是什么样

Kapture 2022-05-07 at 17.09.02.gif

我们先点击低优先级的任务之后开始执行,马上又在点击正常优先级任务可以看到正常优先级任务局把低优先级任务打断了

依次分别执行了各个优先级任务,在看一下浏览器上对每个渲染过程的时间分片过程

1、正常遵循时间切片的任务会符合浏览器每一帧去执行不会超过16.6ms

image.png

2、高优先级同步执行的任务一直占用浏览器时间长达8百多ms所以如果渲染的时候可能会有些卡顿效果(卡顿效果没有录下来想要体验可以找我要demo),这个时候就可以看出时间切片的好处了

image.png

分享就到这里了,如果大家觉得有帮助可以顺手点个赞,如果觉得哪里有问题可以在评论区发言一起讨论一下