这是我参与8月更文挑战的第26天,活动详情查看:8月更文挑战
时间切片原理
时间切片
的本质是模拟实现requestIdleCallback
除去“浏览器重排/重绘”,下图是浏览器一帧中可以用于执行JS
的时机。
一个task(宏任务) -- 队列中全部job(微任务) -- requestAnimationFrame -- 浏览器重排/重绘 -- requestIdleCallback
requestIdleCallback
是在“浏览器重排/重绘”后如果当前帧还有空余时间时被调用的。
浏览器并没有提供其他API
能够在同样的时机(浏览器重排/重绘后)调用以模拟其实现。
唯一能精准控制调用时机的API
是requestAnimationFrame
,他能让我们在“浏览器重排/重绘”之前执行JS
所以 Scheduler
的时间切片
功能是通过task
(宏任务)实现的.
在React
的render
阶段,开启Concurrent Mode
时,每次遍历前,都会通过Scheduler
提供的shouldYield
方法判断是否需要中断遍历,使浏览器有时间渲染:
function workLoopConcurrent() {
// Perform work until Scheduler asks us to yield
while (workInProgress !== null && !shouldYield()) {
performUnitOfWork(workInProgress);
}
}
在Schdeduler
中,为任务分配的初始剩余时间为5ms
。
随着应用运行,会通过fps
动态调整分配给任务的可执行时间。
优先级调度
Scheduler
是独立于React
的包,所以他的优先级
也是独立于React
的优先级
的。
Scheduler
对外暴露了一个方法unstable_runWithPriority。这个方法接受一个优先级
与一个回调函数
,在回调函数
内部调用获取优先级
的方法都会取得第一个参数对应的优先级
:
function unstable_runWithPriority(priorityLevel, eventHandler) {
switch (priorityLevel) {
case ImmediatePriority:
case UserBlockingPriority:
case NormalPriority:
case LowPriority:
case IdlePriority:
break;
default:
priorityLevel = NormalPriority;
}
var previousPriorityLevel = currentPriorityLevel;
currentPriorityLevel = priorityLevel;
try {
return eventHandler();
} finally {
currentPriorityLevel = previousPriorityLevel;
}
}
Scheduler
内部存在5种优先级。
在React
内部凡是涉及到优先级
调度的地方,都会使用unstable_runWithPriority
。
比如,我们知道commit
阶段是同步执行的。可以看到,commit
阶段的起点commitRoot
方法的优先级为ImmediateSchedulerPriority
。
ImmediateSchedulerPriority
即ImmediatePriority
的别名,为最高优先级,会立即执行。
优先级的意义
Scheduler
对外暴露最重要的方法便是unstable_scheduleCallback 该方法用于以某个优先级
注册回调函数。不同优先级
意味着不同时长的任务过期时间。
scheduler调度算法
首先,要明确几点:
-
scheduler是用来做任务调度的
-
所有任务在一个调度生命周期内都有一个过期时间与调度优先级,但是调度优先级最终还是会转换为过期时间,只是过期时间长短的问题,过期时间越短代表越饥饿,优先级也就越高,但已经过期了的任务也会被视为饥饿任务
-
requestAnimationFrameWithTimeout,这是React scheduler的一个超强的函数,它是解决网页选项卡如果在未激活状态下requestAnimationFrame不会被触发的问题,这样的话,调度器是可以在后台继续做调度的,一方面也能提升用户体验,同时后台执行的时间间隔是以100ms为步长,这个是一个最佳实践,100ms是不会影响用户体验同时也不影响CPU能耗的一个折中时间间隔
-
调度优先级分为:
-
- 立即执行优先级,立即过期
- 用户阻塞型优先级,250毫秒后过期
- 空闲优先级,永不过期,可以在任意空闲时间内执行
- 普通优先级,5秒后过期
-
一个调度生命周期分为几个阶段
-
- 调度前
-
- 注册任务队列(环状链表,头接尾,尾接头),按照过期时间从小到大排列,如果当前任务是最饥饿的任务,则排到最前面,并立即开始调度,如果并不是最饥饿的任务,则放到队列中间或者最后面,不做任何操作,等待被调度
- 调度准备
-
- 通过requestAnimationFrame在下一次屏幕刚开始刷新的帧起点时计算当前帧的截止时间(33毫秒内)
- 如果不超过当前帧的截止时间且当前任务没有过期,进入任务调度
- 如果已经超过当前帧的截止时间,但没有过期,进入下一帧,并更新计算帧截止时间,重新判断时间(轮询判断),直到没有任何过期超时或者超时才进入任务调度
- 如果已经超过当前帧的截止时间,同时已经过期,进入过期调度
- 正式调度
-
- 执行调度
-
- 在当前帧的截止时间前批量调用所有任务,不管是否过期
- 过期调度
-
- 批量调用饥饿任务或超时任务的回调,删除任务节点
- 调度完成
- 检查任务队列是否还有任务
-
- 先执行最饥饿的任务
- 如果存在任务,则进入下一帧,进入下一个调度生命周期。