【React 之 Schedule 调度机制二】:多级优先级策略与任务中断恢复原理解析

319 阅读6分钟

react优先级的介绍

就好像我们工作中时常会遇到的一种场景,当我们由A评审会后接到A,B,C三个任务,我们对其评为中优先级,在做需求的过程中,产品突然来了一个紧急需求D,要求D需求要在今天发布上线,这时候D需求就处于最高优先级,我们就需要暂缓A,B,C三个需求,当前需求的排序队列为:

  • D需求处于最高优先级
  • 其次是A
  • 再次是B
  • 最后是C

同理,React实现了一套基于Lane模型的优先级算法,并基于这套算法实现了Batch Update、任务打断/恢复等功能,这些功能由React统一管理,可分为以下五个优先等级(包含各个等级的过期时间):

  • ImmediatePriority -> -1(最高优先级,同步执行,解决饥饿问题也是基于同步执行)
  • UserBlockingPriority -> 250
  • NormalPriorrity -> 5000
  • LowPriority -> 10000
  • IdlePriority -> 1073741823

调度器

Scheduler内部暴露unstable_scheduleCallback(priority, fn, delay)进行任务调度,其中priority指定任务优先级,fn为任务执行回调,delay为延迟时间,指定delay后任务会被放到不同于taskQueue的timeQueue队列里(这个后续文章会详细说),unstable_scheduleCallback(priority, fn, delay)调用后会生成task数据集,代表一个被调度的任务,数据集介绍如下:

const task = {
    expirationTime: startTime + timeout,
    callback: fn
}

其中expirationTime为任务期望执行事件,由任务开始时间(startTimd)和不同优先级所对应的过期时间(timeout)两部分组成

就那最高优先级来举例

const startTime = currentTime = Date.now()
cosnt timeout = priority === "ImmediatePriority" ? -1 : null
startTime + timeout < currentTime

上述代码中基于一个任务开始时间和当前时间,由于最高优先级过期时间最小(为-1),所以startTime+timeout之后的值一定小于当前时间,则该任务会在新的宏任务队列中被优先执行,这就是调度器的原理:即优先级越高期望时间越小,期望时间越小则与当前时间相加结果越小,则执行的越早

结合优先级的调度器执行流程图如下

Snipaste_2025-02-24_23-33-54.png

综合上图,我们可以将调度器的执行流程分为一下几个步骤

  • 用户交互后将work插入workList
  • 对workList进行优先级排序(源码用最小堆算法,但那玩意儿有点费解,先用sort这个API,后续详细阐述)
  • 取出workList第一个任务进行策略逻辑分析
  • 满足策略逻辑分析,调用unstable_scheduleCallback(priority, fn, delay)触发调度,否则return
// 任务队列
const workList = []
// 上一个任务的优先级,默认最低优先级
let prePriority = IdlePriority
// 当前任务
let curTask

function schedule() {
    // 获取当前正在调度的回调
    const cbNode = getFirstCallbackNode();

    // 结合优先级进行任务排序,并取出优先级最高的任务
    // 源码中是通过最小堆,那玩意儿有点复杂,后续单独出一个模块写
    // 这里通过sort来简单模仿
    const curWork = workList.sort((a,b) => {
        return a.priority - b.priority
    })[0]
    
    if(!curWork) {
        // 如果任务队列无任务,则将当前任务置空
        curTask = null
        return
    }
    
    // 取出当前任务优先级
    const {priority: curPriority} = curWork
    
    // 如果当前优先级与上一个任务的优先级相同,不做调度
    if(curPriority === prePriority) return 
    
    // 准备调度更高优先级的work前中止正在进行的work
    cbNode && cancelCallback(cbNode)
    
    // 执行调度
    unstable_scheduleCallback(curPriority, perform.bind(null, curWork))
}
  • 执行情况1:当workList只有一个work的情况,由于callback始终不变,perform循环中断后也会保存这个callback,与新调度的事件进行对比,比对命中则继续执行该callback,直到callback执行完毕结束执行,所以这里可以理解成schedule阶段存在两种循环,分别是"一大一小"循环,大循环包含schedule、scheduler、perform,特点是"始终调度最高优先级任务执行",小循环设计perform方法,特点是"调度一个优先级的事件反复执行"

  • 执行情况2:当调度优先级相同的新work时,schdule会自动return事件,不会中止正在进行的work,所以旧work会一直执行,直到work执行完毕

  • 执行情况3:当插入更高优先级work时,进入schedule后获取到的就是更高优先级的work,所以schedule会中止旧work,派发新事件进入perform,旧事件则回归workList继续排队等候执行,直到成为最高优先级事件或者超过期望执行事件

schedule事件调度后,经优先级排序选择出了最高优先级的事件,交由unstable_scheduleCallback触发perform执行,当Time Slice用尽时,事件任务遍历终止,将控制权还给浏览器,perform代码如下:

function perform(work, didTimeout) {
    // 问题1:didTimeout是什么,为什么它可以更最高优先级一样享用同步执行
    const needSync = work.priority === ImmediatePriority || didTimeout;
    while((needSync || !shouldYiedld) && work.count) {
        work.count--;
    }
    
    // 执行中断,将执行的事件优先级记录下来
    prevPriority = work.priority;
    
    // 如果事件全部执行完毕,将事件剔除出workList
    if(!work.count) {
        const workIndex = workList.indexOf(work)
        workList.splice(workIndex, 1)
        prevPriority = IdlePriority
    }
    
    // 将执行事件记录下来
    const prevCallback = curCallback
    
    // 重新调度
    schedule()
    
    // 获取新的调度事件
    const newCallback = curCallback
    
    // 如果新调度事件和记录的事件是同一个,则接着调用perform消费任务
    if(newCallback && prevCallback === newCallback) {
        return perform.bind(null, work)
    }
}

如问题一所述,didTimeout顾名思义就是超时,即超时任务,假设有一个低优先级任务,执行过程中插入高优先级任务,那么低优先级任务就会被中断执行,后续如果还有高优先级插入,低优先级任务执行会被一直搁置,直到超过事件执行的期望时间,这种问题称为"饥饿问题",react专门为饥饿事件做了优化,当某个任务超出了期望执行时间,设置didTimeout为true,并同步执行该事件不得打断.

shouldYiedld方法由schduler提供,当预留给当前callback的时间用尽(默认是5ms),shouldYiedld() === true,循环终止

结尾总结

通过本文的分析,我们可以将React调度机制的核心逻辑总结为以下四个关键点:

  1. 多级优先级驱动模型

    • React基于Lane模型将任务划分为5级优先级(Immediate/UserBlocking/Normal/Low/Idle),通过expirationTime=startTime+timeout计算任务过期时间

    • 高优先级任务通过更小的timeout值(如ImmediatePriority的-1)确保快速过期,实现调度队列中的插队执行

  2. 时间切片与中断恢复

    • 通过shouldYield()判断5ms时间切片是否耗尽,实现任务可中断式执行

    • 中断时通过prevCallback===newCallback校验实现断点续传,避免重复计算

  3. 饥饿问题解决方案

    • 当低优先级任务didTimeout时会强制同步执行,防止长期被高优先级任务阻塞

    • 通过expirationTime超时检测机制实现任务优先级动态升级,保障系统公平性

这套调度系统使React能在保持60FPS流畅交互的同时,实现复杂更新的渐进式渲染。读者可通过React源码的scheduler包,进一步探索其与并发模式的协同工作原理。