前言
- 学完本篇文章你将对
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// 当没有更高优先级的时候,直接走performif (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,
};
在项目中打印
最后
整理了一套《前端大厂面试宝典》,包含了HTML、CSS、JavaScript、HTTP、TCP协议、浏览器、VUE、React、数据结构和算法,一共201道面试题,并对每个问题作出了回答和解析。 有需要的小伙伴,可以点击文末卡片领取这份文档,无偿分享
部分文档展示:
文章篇幅有限,后面的内容就不一一展示了
有需要的小伙伴,可以点下方卡片免费领取
|