循序渐进,彻底搞懂React调度器原理
前言
- 学完本篇文章你将对
React不同优先级任务调度
有一个全面的认识。
前置知识
学习本片文章前需要知道浏览器一帧中会做哪些事情
-
三部分:执行js、渲染页面、空闲时间。
-
在我们调用setState的时候会创建一个更新任务,当然react内部也会有其他等级的任务。为了避免任务执行时间超过当前帧空闲时间,造成页面卡顿的现象。React利用调度器实现了了在空闲时间内按优先级执行任务。
几个概念
-
work:一个任务,其count属性代表该任务包含count个子组件需要更新。
-
workList:任务队列,包含着React需要执行的所有任务。
-
perform:执行更新流程,当任务执行完毕后,执行schedule,开始下一轮调度。
-
schedule:调度器,从workList中获取一个高优先级任务,交给perform执行。
-
perform与schedule循环往复,实现整个任务调度。
初识任务调度
下面例子中,创建了三个不同等级的任务,每个任务里面都有100个组件需要更新。当然,更新组件的等级是相同的,这里只是作为区分。
html
<style>
#app {
word-break: break-all;
}
</style>
<body>
<div id="app"></div>
<script src="./react优先级调度算法-基础版.js"></script>
</body>
javascript
const workList = [];
const contetnBox = document.querySelector("#app");
let list = [
{
value: "低",
},
{
value: "中",
},
{
value: "高",
},
]
list.forEach(item => {
const btn = document.createElement("button");
btn.innerText = item.value;
contetnBox?.appendChild(btn);
btn.onclick = () => {
// 添加任务
workList.unshift({ ...item, count: 100 })
schedule()
}
})
// 执行任务
const renderComponent = (content) => {
// 更好的观察任务调度,做的延迟效果
let i = 10000000;
while (i) {
i--;
}
const ele = document.createElement("span");
ele.innerText = `${content}`;
contetnBox?.appendChild(ele)
}
// 调度器
function schedule() {
// 获取一个任务,并弹出任务队列
const curWork = workList.pop();
if (curWork) {
// 执行更新流程
perform(curWork)
}
}
// 更新流程
function perform(work) {
while (work.count) {
// count代表这个任务中有count个组件需要更新
work.count -= 1;
renderComponent(work.value)
}
// 当前任务中的组件全部更新完后,继续执行调度
schedule()
}
效果图
在点击按钮后,会向任务队列workList中添加work。调度器开始工作,调度器从workList中获取一个任务,并弹出任务队列中。任务执行完后,调度器继续调度。schedule与perform循环调用,直至任务队列清空。
按优先级顺序执行任务
引入React调度器
引入该包的目的是使用一些方法辅助完成调度工作
项目结构
learn-schedule
├─ package-lock.json
├─ package.json
├─ public
│ └─ index.html
└─ src
└─ index.js
导包
import {
//空闲优先级
unstable_IdlePriority as IdlePriority,
//低优先级
unstable_LowPriority as LowPriority,
//用户阻塞优先级
unstable_UserBlockingPriority as UserBlockingPriority,
//普通优先级
unstable_NormalPriority as NormalPriority,
//立刻执行的优先级
unstable_ImmediatePriority as ImmediatePrity,
// 当某一个preform正在被调度,但是还没被执行时,可以使用该函数进行取消
unstable_cancelCallback as cancelCallback,
// 用于调度preform方法
unstable_scheduleCallback as scheduleCallback,
// 当前帧是否用尽了, 用尽了为true,此时需要中断任务
unstable_shouldYield as shouldYield,
// 返回当前正在调度的任务
unstable_getFirstCallbackNode as getFirstCallbackNode,
// unstable_scheduleCallback的返回值
CallbackNode
} from "scheduler"
添加变量
// 本次schedule进行时,正在调度的任务的优先级
// 设置初始值为undefined
let prevPriority = undefined;
修改schedule方法
function schedule() {
// 当前正在执行的调度任务
const cbNode = getFirstCallbackNode();
// 获取优先级最高的任务
const curWork = workList.sort((node1, node2) => {
return node1.priority - node2.priority;
})[0]
// 如果任务不存在,即任务队列为空
if (!curWork) {
return;
}
const { priority } = curWork;
// 只有本次任务优先级 > 已经正在在执行的任务的优先级,才会中断正在执行的任务
if (priority === prevPriority) {
return;
}
// 此时本次的任务优先级 > 正在执行的任务优先级
// 需要中断正在执行的任务
if (cbNode) {
cancelCallback(cbNode);
}
// 执行任务,以某个优先级来调度某个任务
// 为什么要使用bind,因为scheduleCallback第二个参数是一个回调函数
scheduleCallback(priority, perform.bind(null, curWork))
}
修改work
// 本次schedule进行时,正在调度的任务的优先级
let prevPriority = undefined;
const workList = [];
const contetnBox = document.querySelector("#app");
let list = [
{
priority: IdlePriority,
value: "低",
},
{
priority: LowPriority,
value: "中",
},
{
priority: NormalPriority,
value: "高",
},
]
list.forEach(item => {
const btn = document.createElement("button");
btn.innerText = item.value;
contetnBox?.appendChild(btn);
btn.onclick = () => {
// 添加任务
workList.unshift({ ...item, count: 100 })
schedule()
}
})
修改perform方法
任务执行完的时候,将prevPriority制空
// 更新流程
function perform(work) {
if (work.count === 0) {
const workIndex = workList.indexOf(work)
workList.splice(workIndex, 1)
// 任务执行完的时候,将prevPriority制空
prevPriority = undefined;
}
while (work.count) {
work.count -= 1;
renderComponent(work.value)
}
schedule()
}
效果图
这个时候已经创建了三个不同优先级的任务,当点击多次低优先级任务
后,再点击中优先级任务
,会发现中优先级任务
先被执行。此时已经完成了不同优先级的任务调度。
超出空闲时间时,任务可中断
上一个版本我们可以发现一个问题,当低优先级任务执行过程中,点击中优先级任务的时候,并没有马上中断低优先级的任务,而是等当前正在执行的低优先级任务执行完毕后,才执行中优先级任务。解决该问题的方法就是使perform可中断。
修改perform
function perform(work) {
// 当前任务是否是同步执行
// ImmediatePrity是立即执行优先级,所以需要同步执行
const isSync = work.priority === ImmediatePrity;
// shouldYield判断浏览器当前帧是否剩余空闲时间
while ((isSync || !shouldYield()) && work.count) {
work.count -= 1;
renderComponent(work.value)
}
if (work.count === 0) {
const workIndex = workList.indexOf(work)
workList.splice(workIndex, 1)
// 任务执行完的时候,将prevPriority制空
prevPriority = undefined;
} else {
prevPriority = work.priority;
}
//继续调度
schedule()
}
效果图
当多次点击低优先级任务的时候,再点击中优先级任务,会发现调度器立刻切换到中优先级任务执行,当中优先级任务执行完毕后,会接着执行低优先级任务,并且页面流畅渲染。
优化调度
到这里的时候,其实已经实现了不同优先级的任务调度,但是还有可优化的余地。
观察发现,当任务正在执行时碰到没有空闲时间用完,需要中断执行,这时候会再次执行schedule方法重新进行一系列的调度工作。这个时候会浪费一些性能,其实当没有更高优先级的任务时,我们可以不进行调度,直接再下一个空闲时间内继续执行当前的work。
添加全局变量
// 当前被调度的回调函数
let curCallback = null;
修改schedule
在函数最后一行为curCallback
赋值。curCallback
是一个包裹了当前work的数据结构,后面会讲到。
// 调度器
function schedule() {
// 当前正在执行的调度任务
const cbNode = getFirstCallbackNode();
// 获取优先级最高的任务
const curWork = workList.sort((node1, node2) => {
return node1.priority - node2.priority;
})[0]
// 如果任务不存在,即任务队列为空
if (!curWork) {
curCallback = null;
return;
}
const { priority } = curWork;
// 只有本次任务优先级 > 已经正在在执行的任务的优先级,才会中断正在执行的任务
if (priority <= prevPriority) {
return;
}
// 此时本次的任务优先级 > 正在执行的任务优先级
// 需要中断正在执行的任务
if (cbNode) {
cancelCallback(cbNode);
}
// 执行任务,以某个优先级来调度某个任务
// 为什么要使用bind,因为scheduleCallback第二个参数是一个回调函数
curCallback = scheduleCallback(priority, perform.bind(null, curWork))
}
修改perform
在perform执行的最后,需要重新获取一遍curCallback,此时会触发schedule的priority === prevPriority
判断,当没有更高优先级时,前后两个的curCallback值相等时,此时就可以直接循环perform函数即可,由因为perform是当作回调函数传递给scheduleCallback的,而且当perform返回一个函数时,scheduleCallback会直接执行这个函数,而不会去执行其他调度相关的工作,减少了部分性能开支。
// 更新流程
function perform(work) {
// 当前任务是否是同步执行
// ImmediatePrity是立即执行优先级,所以需要同步执行
const isSync = work.priority === ImmediatePrity;
// shouldYield判断浏览器当前帧是否剩余空闲时间
while ((isSync || !shouldYield()) && work.count) {
work.count -= 1;
renderComponent(work.value)
}
if (work.count === 0) {
const workIndex = workList.indexOf(work)
workList.splice(workIndex, 1)
prevPriority = undefined;
} else {
prevPriority = work.priority;
}
//存储当前回调
const prevCallback = curCallback
//继续调度
schedule()
//获取新的回调
const newCallback = curCallback
// 当没有更高优先级的时候,直接走perform
if (prevCallback === newCallback) {
return perform.bind(null, work)
}
}
补充:curCallback是什么
其实就是对当前work信息的一个封装
在React源码源码中长这样
type Task = {
id: number,
callback: Callback | null,
priorityLevel: PriorityLevel,
startTime: number,
expirationTime: number,
sortIndex: number,
isQueued?: boolean,
};
在项目中打印
最后
转载自:https://juejin.cn/post/7150916189516988447