likes
comments
collection
share

想想react会怎么做(7)之 任务调度我们知道在react中有很多任务(比如交互触发的更新)需要执行,那边react是

作者站长头像
站长
· 阅读数 37

我们知道在react中有很多任务(比如交互触发的更新)需要执行,那边react是怎么调度和执行任务的呢,我们通过两个demo来学习一下

同步调度

我们先来写一个同步调度的demo,这里的同步调度就是用一个任务队列来调度任务,不断从队列中取出任务执行,然后等所有任务队列中的任务执行完毕之后就结束调度

这里写同步调度的demo目的是为了让大家先了解一下任务调度的概念,为了后续的异步可中断的调度demo做铺垫

实现步骤

  1. 这里我们定义一个任务(Task),它有一个count字段,代表它执行的次数,然后定义一个任务队列用来存放任务,这里我们用数组实现任务队列即可(unshift入队,pop出队)
interface Task{
    /** 执行次数 */
    count:number
}

/** 任务队列 */
const taskQueue:Task[] = []
  1. 然后通过一个按钮(交互)来向我们任务队列中添加一个任务(默认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()
    })
}
  1. 使用scheduleTaskQueue函数调度任务(任务队列中取任务然后执行)
function scheduleTaskQueue(){
    // 从任务队列取出当前要执行的任务
    const curTask = taskQueue.pop()
    if(curTask){
        executeTask(curTask)
    }
}
  1. 使用executeTask执行任务,执行完成之后继续使用scheduleTaskQueue调度任务
function executeTask(task:Task){
    // 执行次数不为空就一直执行
    while(task.count){
        task.count--   
        executeTaskOnce()
    }
    // 执行完所有次数后,开始调度下一个任务
    scheduleTaskQueue()
}
  1. 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图:

想想react会怎么做(7)之 任务调度我们知道在react中有很多任务(比如交互触发的更新)需要执行,那边react是

因为我们直接是同步调度然后同步执行,可以看到全是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:调度的任务的类型

实现步骤

  1. 相对于同步的任务,我们需要定义一个具有优先级和唯一标识的任务,同样需要定义一个任务队列
interface Task{
    /** 执行次数 */
    count:number
    /** 任务的优先级,数值越小,优先级越高 */
    priority: IPriority
    /** 任务ID */
    id: string
}

/** 任务默认执行次数 */
const TaskDefaultCount = 200

/** 任务队列 */
const taskQueue:Task[] = []
  1. 既然有优先级的概率,我们也需要有几个不同的按钮(交互)来插入不同优先级的任务
<!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()
  1. 因为存在中断,所以需要用preTaskPriority和curScheduleCallBack两个变量分别存储上被中断任务的优先级和当前被调度的任务
/** 记录被中断任务的优先级,初始值为IdlePriority */
let preTaskPriority = IPriority.IdlePriority

/** 当前调度的回调函数,初始值为null */
let curScheduleCallBack:CallbackNode|null = null
  1. 调度任务队列逻辑
// 调度函数
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)
    )
}
  1. 执行任务的逻辑,也就是被调度器调度的实际方法
// 执行任务函数
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)   
    }
}
  1. 任务具体执行的逻辑如下
// 执行一次任务
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)

想想react会怎么做(7)之 任务调度我们知道在react中有很多任务(比如交互触发的更新)需要执行,那边react是

插入用户阻塞优先级任务

一些小任务和一个长任务

想想react会怎么做(7)之 任务调度我们知道在react中有很多任务(比如交互触发的更新)需要执行,那边react是

插入同步优先级任务

一个长任务

想想react会怎么做(7)之 任务调度我们知道在react中有很多任务(比如交互触发的更新)需要执行,那边react是

高优先级中断低优先级

这里我们用用户阻塞优先级来中断普通优先级,表现为:点击插入用户阻塞优先级任务时高优先级的直接执行,等用户阻塞优先级的任务执行完成之后再执行普通优先级任务

想想react会怎么做(7)之 任务调度我们知道在react中有很多任务(比如交互触发的更新)需要执行,那边react是

转载自:https://juejin.cn/post/7410599171122118695
评论
请登录