想想react会怎么做(7)之 任务调度我们知道在react中有很多任务(比如交互触发的更新)需要执行,那边react是
我们知道在react中有很多任务(比如交互触发的更新)需要执行,那边react是怎么调度和执行任务的呢,我们通过两个demo来学习一下
同步调度
我们先来写一个同步调度的demo,这里的同步调度就是用一个任务队列来调度任务,不断从队列中取出任务执行,然后等所有任务队列中的任务执行完毕之后就结束调度
这里写同步调度的demo目的是为了让大家先了解一下任务调度的概念,为了后续的异步可中断的调度demo做铺垫
实现步骤
- 这里我们定义一个任务(Task),它有一个count字段,代表它执行的次数,然后定义一个任务队列用来存放任务,这里我们用数组实现任务队列即可(unshift入队,pop出队)
interface Task{
/** 执行次数 */
count:number
}
/** 任务队列 */
const taskQueue:Task[] = []
- 然后通过一个按钮(交互)来向我们任务队列中添加一个任务(默认count为50000)
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>同步任务调度测试</title>
</head>
<body>
<div class="container">
<button class="insert_button">
点击我向任务队列插入任务
</button>
</div>
</body>
<script type="module" src="./sync.ts"></script>
</html>
/** 插入任务的按钮元素 */
const insertButtonElement = document.querySelector('.insert_button')
if(insertButtonElement){
// 监听点击事件,插入任务
insertButtonElement.addEventListener('click', () => {
// 初始化一个执行次数为1000的任务
const newTask = {
count: 50000
}
taskQueue.unshift(newTask)
// 开始调度任务队列
scheduleTaskQueue()
})
}
- 使用scheduleTaskQueue函数调度任务(任务队列中取任务然后执行)
function scheduleTaskQueue(){
// 从任务队列取出当前要执行的任务
const curTask = taskQueue.pop()
if(curTask){
executeTask(curTask)
}
}
- 使用executeTask执行任务,执行完成之后继续使用scheduleTaskQueue调度任务
function executeTask(task:Task){
// 执行次数不为空就一直执行
while(task.count){
task.count--
executeTaskOnce()
}
// 执行完所有次数后,开始调度下一个任务
scheduleTaskQueue()
}
- executeTask中的逻辑就是向页面的容器中插入一个div,div有一些样式方便查看效果
function executeTaskOnce(){
// 新建一个span元素
const newDivElement = document.createElement('span')
// 设置该span的样式(宽,高,背景,边框)
newDivElement.style.cssText="width:2px;height:2px;background-color:red;border:1px solid black;display:inline-block"
// 在container中插入该span元素
containerElement?.append(newDivElement)
整体代码如下:
interface Task{
/** 执行次数 */
count:number
}
/** 任务队列 */
const taskQueue:Task[] = []
/** 插入任务的按钮元素 */
const insertButtonElement = document.querySelector('.insert_button')
/** container元素 */
const containerElement = document.querySelector('.div_container')
if(insertButtonElement){
// 监听点击事件,插入任务
insertButtonElement.addEventListener('click', () => {
// 初始化一个执行次数为1000的任务
const newTask = {
count: 50000
}
taskQueue.unshift(newTask)
// 开始调度任务队列
scheduleTaskQueue()
})
}
// 调度函数
function scheduleTaskQueue(){
// 从任务队列取出当前要执行的任务
const curTask = taskQueue.pop()
if(curTask){
executeTask(curTask)
}
}
// 执行任务函数
function executeTask(task:Task){
// 执行次数不为空就一直执行
while(task.count){
task.count--
executeTaskOnce()
}
// 执行完所有次数后,开始调度下一个任务
scheduleTaskQueue()
}
// 执行一次任务
function executeTaskOnce(){
// 这里就向container中插入div元素
const newDivElement = document.createElement('span')
// 设置div的样式
newDivElement.style.cssText="width:2px;height:2px;background-color:red;border:1px solid black;display:inline-block"
containerElement?.append(newDivElement)
}
效果展示
我们多点击按钮几次,然后查看performance图:
因为我们直接是同步调度然后同步执行,可以看到全是long task,而这样的一系列 long task 执行会阻塞用户的操作
异步可中断的调度
这里的异步指的是通过时间切片来分配任务执行的时间,可中断指的是任务具有优先级,高优先级的任务可以打断低优先级的任务
调度的核心实现就是使用scheduler库,也是react中用于调度任务的库
我来简单介绍一下这次demo使用到的scheduler中的方法和常量
优先级常量:
- unstable_ImmediatePriority: 同步更新优先级
- unstable_UserBlockingPriority: 例如用户点击事件
- unstable_NormalPriority: 正常优先级
- unstable_LowPriority: 低优先级
- unstable_IdlePriority: 空闲时刻的优先级
方法:
- unstable_scheduleCallback:用来调度任务
- unstable_shouldYield:判断时间切片是否耗尽
- unstable_getFirstCallbackNode:获取当前正在执行的任务
- unstable_cancelCallback:取消调度当前的任务
类型:
- CallbackNode:调度的任务的类型
实现步骤
- 相对于同步的任务,我们需要定义一个具有优先级和唯一标识的任务,同样需要定义一个任务队列
interface Task{
/** 执行次数 */
count:number
/** 任务的优先级,数值越小,优先级越高 */
priority: IPriority
/** 任务ID */
id: string
}
/** 任务默认执行次数 */
const TaskDefaultCount = 200
/** 任务队列 */
const taskQueue:Task[] = []
- 既然有优先级的概率,我们也需要有几个不同的按钮(交互)来插入不同优先级的任务
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>测试</title>
</head>
<body>
<div class="container">
<button class="insert_immediate_button">
点击我向任务队列插入同步任务
</button>
<button class="insert_userBlocking_button">
点击我向任务队列插入用户阻塞任务
</button>
<button class="insert_normal_button">
点击我向任务队列插入普通任务
</button>
<button class="insert_low_button">
点击我向任务队列插入低优任务
</button>
<div class="div_container"></div>
</div>
</body>
<script type="module" src="./cocurrent.ts"></script>
</html>
// 做一些初始化操作(绑定交互事件)
function init(){
/** 插入同步任务的按钮元素 */
const immediateButton = document.querySelector('.insert_immediate_button')
/** 插入用户阻塞的按钮元素 */
const userBlockingButton = document.querySelector('.insert_userBlocking_button')
/** 插入普通的按钮元素 */
const normalButton = document.querySelector('.insert_normal_button')
/** 插入低优的按钮元素 */
const lowButton = document.querySelector('.insert_low_button')
immediateButton?.addEventListener('click', () => {
// 新建同步更新优先级任务到任务队列,并开始调度
initTask(IPriority.ImmediatePriority)
})
userBlockingButton?.addEventListener('click', () => {
// 新建用户阻塞优先级任务到任务队列,并开始调度
initTask(IPriority.UserBlockingPriority)
})
normalButton?.addEventListener('click', () => {
// 新建普通优先级任务到任务队列,并开始调度
initTask(IPriority.NormalPriority)
})
lowButton?.addEventListener('click', () => {
// 新建低优先级任务到任务队列,并开始调度
initTask(IPriority.LowPriority)
})
}
// 根据优先级初始化任务然后塞入队列
function initTask(priority:IPriority){
const newTask = {
count: TaskDefaultCount,
priority: priority,
id: uuidv4()
}
taskQueue.unshift(newTask)
// 开始调度任务队列
scheduleTaskQueue()
}
init()
- 因为存在中断,所以需要用preTaskPriority和curScheduleCallBack两个变量分别存储上被中断任务的优先级和当前被调度的任务
/** 记录被中断任务的优先级,初始值为IdlePriority */
let preTaskPriority = IPriority.IdlePriority
/** 当前调度的回调函数,初始值为null */
let curScheduleCallBack:CallbackNode|null = null
- 调度任务队列逻辑
// 调度函数
function scheduleTaskQueue(){
// 获取当前调度的任务
const curCallBackNode = unstable_getFirstCallbackNode()
// 从任务队列取出优先级最高的任务, 也就是priority最小的任务
const curTask = taskQueue.sort((task1,task2)=>task1.priority-task2.priority)[0]
// 如果当前已经没有任务了,做一些初始化后直接 return
if(!curTask){
curScheduleCallBack = null
curCallBackNode && unstable_cancelCallback(curCallBackNode)
return;
}
// 获取当前选出任务的优先级
const { priority: curTaskPriority } = curTask
// 如果当前任务的优先级和被中断的任务的优先级相同,则不重新调度,直接return
if(curTaskPriority === preTaskPriority){
return;
}
// 更高优先级的任务会走到下面的的逻辑
// 取消当前的回调,下面会调度更高优先级的回调
curCallBackNode && unstable_cancelCallback(curCallBackNode)
// 使用scheduleCallback调度任务, 并记录当前的回调函数
curScheduleCallBack = unstable_scheduleCallback(
curTaskPriority,
executeTask.bind(null, curTask)
)
}
- 执行任务的逻辑,也就是被调度器调度的实际方法
// 执行任务函数
function executeTask(
// 当前要执行的任务
task:Task,
// 是否过期(解决饥饿问题)
didTimeout:boolean = false
){
// 判断是否需要同步执行(如果优先级就是同步,或者马上要过期了就同步执行)
const isNeedSync = task.priority === unstable_ImmediatePriority || didTimeout
// 如果需要同步执行或者时间切片没有用尽的情况下执行任务
while((isNeedSync || !unstable_shouldYield()) && task.count){
task.count--
executeTaskOnce(task.priority)
}
// 记录当前的任务优先级
preTaskPriority = task.priority
// 如果是执行被中断||执行完所有次数后走下面的逻辑
// 执行完所有次数后,将当前任务从任务队列中移除
if(!task.count){
// 通过任务ID找到当前任务的索引
const curIndex = taskQueue.findIndex(item=>item.id === task.id)
// 根据索引删除当前任务
taskQueue.splice(curIndex, 1)
// 任务次数都执行完之后,重置preTaskPriority
preTaskPriority = IPriority.IdlePriority
}
const preScheduleCallBack = curScheduleCallBack
scheduleTaskQueue()
const newScheduleCallBack = curScheduleCallBack
// 如果调度函数没有变化,则返回原函数
if(newScheduleCallBack && preScheduleCallBack === newScheduleCallBack){
return executeTask.bind(null,task)
}
}
- 任务具体执行的逻辑如下
// 执行一次任务
function executeTaskOnce(priority:IPriority){
console.log(`执行了任务,优先级为${priority}`)
// 新建span元素
const newSpanElement = document.createElement('span')
// 根据优先级设置该span的样式
switch(priority){
// 同步优先级,红色背景
case IPriority.ImmediatePriority:
newSpanElement.style.cssText="width:10px;height:10px;background-color:red;border:1px solid black;display:inline-block"
break;
// 用户阻塞优先级:绿色背景
case IPriority.UserBlockingPriority:
newSpanElement.style.cssText="width:10px;height:10px;background-color:green;border:1px solid black;display:inline-block"
break;
// 普通阻塞优先级:蓝色背景
case IPriority.NormalPriority:
newSpanElement.style.cssText="width:10px;height:10px;background-color:blue;border:1px solid black;display:inline-block"
break;
// 低优先级:黄色背景
case IPriority.LowPriority:
newSpanElement.style.cssText="width:10px;height:10px;background-color:yellow;border:1px solid black;display:inline-block"
break;
// 空闲时执行优先级:灰色
case IPriority.IdlePriority:
newSpanElement.style.cssText="width:10px;height:10px;background-color:gray;border:1px solid black;display:inline-block"
break;
}
// 为了效果明显,做一些耗时的动作
doSomeDelayWork(20000000)
// 向容器中插入这个span
containerElement?.append(newSpanElement)
}
整体代码如下:
import {
unstable_ImmediatePriority,
unstable_UserBlockingPriority,
unstable_NormalPriority,
unstable_LowPriority,
unstable_IdlePriority,
unstable_scheduleCallback,
unstable_shouldYield,
CallbackNode,
unstable_getFirstCallbackNode,
unstable_cancelCallback
} from 'scheduler'
import { v4 as uuidv4 } from 'uuid';
/** scheduler定义的五种优先级
* unstable_ImmediatePriority: 同步更新优先级
* unstable_UserBlockingPriority: 例如用户点击事件
* unstable_NormalPriority: 正常优先级
* unstable_LowPriority: 低优先级
* unstable_IdlePriority: 空闲时刻的优先级
*/
enum IPriority {
ImmediatePriority = unstable_ImmediatePriority,
UserBlockingPriority = unstable_UserBlockingPriority,
NormalPriority = unstable_NormalPriority,
LowPriority = unstable_LowPriority,
IdlePriority = unstable_IdlePriority
}
interface Task{
/** 执行次数 */
count:number
/** 任务的优先级,数值越小,优先级越高 */
priority: IPriority
/** 任务ID */
id: string
}
/** 任务默认执行次数 */
const TaskDefaultCount = 200
/** 任务队列 */
const taskQueue:Task[] = []
/** container元素 */
const containerElement = document.querySelector('.div_container')
/** 记录上一个任务的优先级 */
let preTaskPriority = IPriority.IdlePriority
/** 当前调度的回调函数 */
let curScheduleCallBack:CallbackNode|null = null
function init(){
/** 插入同步任务的按钮元素 */
const immediateButton = document.querySelector('.insert_immediate_button')
/** 插入用户阻塞的按钮元素 */
const userBlockingButton = document.querySelector('.insert_userBlocking_button')
/** 插入普通的按钮元素 */
const normalButton = document.querySelector('.insert_normal_button')
/** 插入低优的按钮元素 */
const lowButton = document.querySelector('.insert_low_button')
immediateButton?.addEventListener('click', () => {
// 初始化一个执行次数为100的任务
const newTask = {
count: TaskDefaultCount,
priority: IPriority.ImmediatePriority,
id: uuidv4()
}
taskQueue.unshift(newTask)
// 开始调度任务队列
scheduleTaskQueue()
})
userBlockingButton?.addEventListener('click', () => {
// 初始化一个执行次数为100的任务
const newTask = {
count: TaskDefaultCount,
priority: IPriority.UserBlockingPriority,
id: uuidv4()
}
taskQueue.unshift(newTask)
// 开始调度任务队列
scheduleTaskQueue()
})
normalButton?.addEventListener('click', () => {
// 初始化一个执行次数为100的任务
const newTask = {
count: TaskDefaultCount,
priority: IPriority.NormalPriority,
id: uuidv4()
}
taskQueue.unshift(newTask)
// 开始调度任务队列
scheduleTaskQueue()
})
lowButton?.addEventListener('click', () => {
// 初始化一个执行次数为100的任务
const newTask = {
count: TaskDefaultCount,
priority: IPriority.LowPriority,
id: uuidv4()
}
taskQueue.unshift(newTask)
// 开始调度任务队列
scheduleTaskQueue()
})
}
init()
// 调度函数
function scheduleTaskQueue(){
const curCallBackNode = unstable_getFirstCallbackNode()
// 从任务队列取出优先级最高的任务, priority最小的
const curTask = taskQueue.sort((task1,task2)=>task1.priority-task2.priority)[0]
// 如果当前已经没有任务了,做一些初始化
if(!curTask){
curScheduleCallBack = null
curCallBackNode && unstable_cancelCallback(curCallBackNode)
return;
}
const { priority: curTaskPriority } = curTask
// 如果当前任务的优先级和上一个任务的优先级相同,则不重新调度
if(curTaskPriority === preTaskPriority){
return;
}
// 更高优先级的任务会走到这里的逻辑
// 取消当前的回调,调度更高优先级的回调
curCallBackNode && unstable_cancelCallback(curCallBackNode)
// 使用scheduleCallback调度任务, 并记录当前的回调函数
curScheduleCallBack = unstable_scheduleCallback(
curTaskPriority,
executeTask.bind(null, curTask)
)
}
// 执行任务函数
function executeTask(
// 当前执行任务
task:Task,
// 是否过期(解决饥饿问题)
didTimeout:boolean = false
){
// 是否需要同步执行(如果优先级就是同步,或者马上要过期了就同步执行)
const isNeedSync = task.priority === unstable_ImmediatePriority || didTimeout
// 如果需要同步执行或者时间切片没有用尽的情况下执行任务
while((isNeedSync || !unstable_shouldYield()) && task.count){
task.count--
executeTaskOnce(task.priority)
}
// 记录当前被中断的任务优先级
preTaskPriority = task.priority
// 如果是执行被中断||执行完所有次数后走下面的逻辑
// 执行完所有次数后,将当前任务从任务队列中移除
if(!task.count){
// 通过任务ID找到当前任务的索引
const curIndex = taskQueue.findIndex(item=>item.id === task.id)
// 根据索引删除当前任务
taskQueue.splice(curIndex, 1)
// 任务次数都执行完之后,重置preTaskPriority
preTaskPriority = IPriority.IdlePriority
}
const preScheduleCallBack = curScheduleCallBack
scheduleTaskQueue()
const newScheduleCallBack = curScheduleCallBack
// 如果调度函数没有变化,则返回原函数
if(newScheduleCallBack && preScheduleCallBack === newScheduleCallBack){
return executeTask.bind(null,task)
}
}
// 执行一次任务
function executeTaskOnce(priority:IPriority){
console.log(`执行了任务,优先级为${priority}`)
// 这里就向container中插入div元素
const newDivElement = document.createElement('span')
// 根据优先级设置div的样式
switch(priority){
// 同步优先级
case IPriority.ImmediatePriority:
newDivElement.style.cssText="width:10px;height:10px;background-color:red;border:1px solid black;display:inline-block"
break;
// 用户阻塞优先级
case IPriority.UserBlockingPriority:
newDivElement.style.cssText="width:10px;height:10px;background-color:green;border:1px solid black;display:inline-block"
break;
// 普通优先级
case IPriority.NormalPriority:
newDivElement.style.cssText="width:10px;height:10px;background-color:blue;border:1px solid black;display:inline-block"
break;
// 低优先级
case IPriority.LowPriority:
newDivElement.style.cssText="width:10px;height:10px;background-color:yellow;border:1px solid black;display:inline-block"
break;
// 空闲时优先级
case IPriority.IdlePriority:
newDivElement.style.cssText="width:10px;height:10px;background-color:gray;border:1px solid black;display:inline-block"
break;
}
// 为了效果明显,做一些耗时的动作
doSomeDelayWork(20000000)
// 往容器中插入div
containerElement?.append(newDivElement)
}
function doSomeDelayWork(cnt:number){
while(cnt--){}
}
效果展示
我们同样插入任务,然后打开performance查看效果
插入普通优先级和低优先级任务
发现完全没有任何一个长任务(时间超过50ms)
插入用户阻塞优先级任务
一些小任务和一个长任务
插入同步优先级任务
一个长任务
高优先级中断低优先级
这里我们用用户阻塞优先级来中断普通优先级,表现为:点击插入用户阻塞优先级任务时高优先级的直接执行,等用户阻塞优先级的任务执行完成之后再执行普通优先级任务
转载自:https://juejin.cn/post/7410599171122118695