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之后的值一定小于当前时间,则该任务会在新的宏任务队列中被优先执行,这就是调度器的原理:即优先级越高期望时间越小,期望时间越小则与当前时间相加结果越小,则执行的越早
结合优先级的调度器执行流程图如下
综合上图,我们可以将调度器的执行流程分为一下几个步骤
- 用户交互后将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调度机制的核心逻辑总结为以下四个关键点:
-
多级优先级驱动模型
-
React基于Lane模型将任务划分为5级优先级(Immediate/UserBlocking/Normal/Low/Idle),通过
expirationTime=startTime+timeout计算任务过期时间 -
高优先级任务通过更小的timeout值(如ImmediatePriority的-1)确保快速过期,实现调度队列中的插队执行
-
-
时间切片与中断恢复
-
通过
shouldYield()判断5ms时间切片是否耗尽,实现任务可中断式执行 -
中断时通过
prevCallback===newCallback校验实现断点续传,避免重复计算
-
-
饥饿问题解决方案
-
当低优先级任务
didTimeout时会强制同步执行,防止长期被高优先级任务阻塞 -
通过
expirationTime超时检测机制实现任务优先级动态升级,保障系统公平性
-
这套调度系统使React能在保持60FPS流畅交互的同时,实现复杂更新的渐进式渲染。读者可通过React源码的scheduler包,进一步探索其与并发模式的协同工作原理。