scheduler
模块用于管理重绘完成后回调的执行逻辑。从输出分析,对整个调度过程进行梳理。
基础前提
浏览器渲染与事件循环
浏览器采用多进程架构,包含浏览器主进程、渲染进程、插件进程、GPU进程。每开启一个tab时浏览器就会开启一个渲染进程,该进程里包含多个线程:负责运行js
、dom
和css
计算和页面渲染的主线程,运行worker
的工作线程等。主线程解析html
时,遇到script
标签,会暂停html
的解析,并开始加载、解析并执行js
代码、为了调度事件、用户交互、渲染、网络请求这些操作,主线程会通过事件循环来处理。事件循环的过程为:
-
同步任务
-
一个宏任务
-
清空微任务队列
-
判断是否渲染视图(是否有重排、重绘、渲染间隔是否达到
16.7ms
等),为真则渲染视图,否则跳至步骤1,页面渲染前调用requestAnimationFrame
回调函数,最后判断是否启动空闲时间算法,如果启动就调用requestIdleCallback
常见的宏任务:事件回调、xhr
回调、定时器、I/O
、MessageChannel
常见的微任务:Promise
、Generator
、Async/Await
、MutationObserver
时间片
js
在浏览器中的执行是单线程的,长时间的js
任务执行可能会阻塞其他浏览器任务,如页面渲染、用户交互等,有可能会造成用户的卡顿感。schedule
中采用时间分片的策略,将任务细化为不同的优先级,利用浏览器的空闲时间进行任务的执行保证UI
操作的流畅。浏览器的调度API
主要分为两种,高优先级的requestAnimationFrame
与低优先级的requestIdleCallback
。
将js
任务分解到时间片中执行后,一次时间循环最多只执行一个时间片,若还有未完成的任务,将这些任务放到后面的事件循环的时间片中执行,保证不会阻塞其他的浏览器任务。
requestAnimationFrame
requestAnimationFrame
传入一个回调函数作为参数,该回调函数会在浏览器下一次重绘之前执行。回调函数执行次数通常是每秒60次,在大多数遵循w3c建议的浏览器中,回调函数执行次数通常与浏览器屏幕刷新次数相匹配。大多数浏览器中,当 requestAnimationFrame
运行在后台标签页或隐藏的iframe
中时,requestAnimationFrame
会被暂停调用。
requestAnimationFrame
函数接收一个接收DOMHighResTimeStamp
参数的callback
函数作为参数,返回一个requestId
供cancelAnimationFrame
以取消。
requestIdleCallback
浏览器每秒一般60帧,帧与帧的间隔成为时间片,长度为 1000 / 60
约16ms
,如果一帧渲染完成的时间小于16ms
,这个时间片就有空闲时间。空闲时间会被执行requestIdleCallback
的回调函数。当超过timeout
时间还不执行callback
时,callback
将会被强制执行,造成的后果是,阻塞本地渲染,延长渲染时间,造成卡顿、延迟等。
callback
函数接收IdleDeadline
接口类型的参数,是一个对象,包含两个属性
-
didTimeout
,布尔值,表示任务是否超时 -
timeRemaining
,表示当前时间片剩余的时间。
requestIndleCallback
会返回一个id
,传入cancelIdleCallback
可结束对应的回调。
使用时间片的实现:
scheduler
每执行一次performWorkUntilDeadline
函数表示执行了一个时间片,执行该函数前会通过MessageChannel
或setTimeout
将该函数放入宏任务队列,优先使用MessageChannel
。
每执行一个时间片时,将时间片长度 yieldInterval
和过期时间deadline
放入执行的任务中,并通返回值判断是否存在未执行完的任务(表示时间片已用完)。若存在为执行完的任务,则让任务在下次事件循环继续执行。
export {
ImmediatePriority as unstable_ImmediatePriority, // 1
UserBlockingPriority as unstable_UserBlockingPriority, // 2
NormalPriority as unstable_NormalPriority, // 3
IdlePriority as unstable_IdlePriority, // 5
LowPriority as unstable_LowPriority, // 4
unstable_runWithPriority,
unstable_next,
unstable_scheduleCallback,
unstable_cancelCallback,
unstable_wrapCallback,
unstable_getCurrentPriorityLevel,
shouldYieldToHost as unstable_shouldYield,
unstable_requestPaint,
unstable_continueExecution,
unstable_pauseExecution,
unstable_getFirstCallbackNode,
getCurrentTime as unstable_now,
forceFrameRate as unstable_forceFrameRate,
};
export const unstable_Profiling = enableProfiling
? {
startLoggingProfilingEvents,
stopLoggingProfilingEvents,
}
: null;
输出函数分析
任务优先级
react
内对任务优先级的定义。Scheduler
中任务有不同的优先级,每个优先级有对应的过期时间,在生成任务时根据优先级和创建时间生成任务的过期时间,任务过期后才会放入taskQueue
执行,否则放入timerQueue
等待执行。
优先级 | 值 | 含义 | 过期时间 | 过期时间的值 |
---|---|---|---|---|
NoPriority |
0 | 无优先级 | ||
ImmediatePriority |
1 | 最高优先级 | IMMEDIATE_PRIORITY_TIMEOUT |
-1 |
UserBlockingPriority |
2 | 用户阻塞型优先级 | USER_BLOCKING_PRIORITY_TIMEOUT |
250 |
NormalPriority |
3 | 普通优先级 | NORMAL_PRIORITY_TIMEOUT |
5000 |
LowPriority |
4 | 低优先级 | LOW_PRIORITY_TIMEOUT |
10000 |
IdlePriority |
5 | 空闲优先级 | IDLE_PRIORITY_TIMEOUT |
maxSigned31BitInt = Math.pow(2, 30) - 1 |
环境中设置变量分析
//任务存储在小顶堆上
var taskQueue = [] // 任务队列
var timerQueue = []; // 延时任务队列
var taskIdCounter = 1; // 递增id计数器, 用于维护插入顺序
var isSchedulerPaused = false; // 暂停调度程序,用于调试
var currentTask = null; // 当前任务
var currentPriorityLevel = NormalPriority; //3 当前执行任务的优先级
var isPerformingWork = false; // 是否正在执行任务,在执行工作时设置的, 以防止重新进入
var isHostCallbackScheduled = false; // 是否有主任务正在执行,是否调度了 taskQueue, isHostCallbackScheduled为true后才把时间片放到宏任务队列,之后开始执行任务
var isHostTimeoutScheduled = false; // 是否有延时任务正在执行,是否调度了 timerQueue, 设置了 timeout回调
SchedulerHostConfig
let requestHostCallback; // 请求回调
let cancelHostCallback; // 取消回调
let requestHostTimeout; // 请求超时
let cancelHostTimeout; // 取消超时
let shouldYieldToHost;
let requestPaint; // 请求绘制
let getCurrentTime; // 获取当前时间, 优先用 performance.now(), 或者用 Date.now() - 初始时间
let forceFrameRate; // 强制帧率
-
如果
Scheduler
运行在非DOM
环境中,使用setTimeout
回退到一个简单的实现。环境中检测不到window
或者不支持MessageChannel
时:-
requestHostCallback
-
cancelHostCallback
-
requestHostTimeout
-
cancelHostTimeout
-
shouldYieldToHost
-
requestPaint
-
forceFrameRate
-
unstable_runWithPriority
unstable_runWithPriority(*priorityLevel*, *eventHandler*)
主要逻辑:
-
将
currentPriorityLevel
设置为priorityLevel
,然后执行eventHandler
-
最后将
currentPriorityLevel
改回之前的值
unstable_next
unstable_next(*eventHandler*)
主要逻辑:
-
currentPriorityLevel
高于NormalPriority
情况下设置为NormalPriority
,否则保持当前优先级 -
执行
eventHandler
-
最后将
currentPriorityLevel
改回之前的值
unstable_scheduleCallback
整体流程:
-
scheduler
中任务有不同优先级,每个优先级有对应的过期时间,在生成任务根据优先级和创建事件生成任务的过期时间,任务过期后才会放入taskQueue
执行,否则放入timerQueue
等待执行 -
TaskQueue
和TimerQueue
是两个用小顶堆实现的具有优先级的任务队列。TaskQueue
中优先级的索引是expirationTime
,TimerQueue
中优先级的索引使用的是startTime
。SchedulerMinHeap.js
中实现了对小顶堆的peek
、pop
、push
方法。使用advanceTimers
方法可以依据指定的时间和任务的开始时间将TimerQueue
中的任务更新到TaskQueue
中,更新时会同时更新索引使用的值为expirationTime
-
通过
unstable_scheduleCallback
添加任务,生成一个任务对象。任务对象包含任务id
、任务执行函数、优先级、开始时间、过期时间和在队列中的顺序。任务通过传入参数中的delay
属性值来判断该任务是同步任务还是异步任务。 -
若生成的任务是同步任务,则将该任务推入
taskQueue
-
如果当前
taskQueue
是未被调度且任务未被执行,则使用requestHostCallback
调用flushWork
方法 -
requestHostCallback
方法内会触发message
事件,performWorkUntilDeadline
函数作为message
事件的回调将推入事件循环的宏任务队列 -
performWorkUntilDeadline
方法会执行一个时间片的任务,时间片用完后会判断是否还有未执行的任务,如果有则再次触发message
事件 -
flushWork
先取消timerQueue
的回调,之后设置isPerformingWork
为false
,并调用workLoop
方法执行taskQueue
中的任务 -
workLoop
会不断取出taskQueue
中的任务,直到执行完所有的任务或者执行完所有超时的任务且时间片已用完。最后若存在未执行完的任务,则返回true
,否则重新设置timerQueue
中的回调,并返回false
-
-
若生成的任务是异步任务,则将任务推入
timerQueue
。如果当前taskQueue
为空且新任务在timerQueue
中优先级最高,使用requestHostTimeout
调度handleTimeout
方法 -
handleTimeout
首先判断当前是否在调度taskQueue
,若没有在调度,则判断taskQueue
是否为空,如果不为空,则调度taskQueue
,否则调度timerQueue
unstable_scheduleCallback(*priorityLevel*, *callback*, *options*)
主要逻辑为,根据输入返回newTask
。
- 根据传入的
options
更新startTime
,根据传入的priorityLevel
更新timeout
,然后计算expireTime
,定义newTask
var newTask = {
id: taskIdCounter++,
callback,
priorityLevel,
startTime,
expirationTime,
sortIndex: -1,
};
-
对于延时任务,
startTime > currentTime
,startTime
设置为sortIndex
,将任务添加到timerQueue
队列,如果taskQueue
为空且timerQueue
只有newTask
一个延时任务。是否有延时任务正在执行,如果有,清除定时器,否则将isHostTimeoutScheduled
设置为true
。执行requestHostTimeout
,延时处理handleTimeout
。-
requestHostTimeout
逻辑:接收callback
和ms
,经过ms
后执行callback
,将getCurrentTime()
的值传入. -
handleTimeout
逻辑:isHostTimeoutScheduled
设置为false
,执行advanceTimers
。如果isHostCallbackScheduled
为false
,即没有主任务正在执行,设置isHostCallbackScheduled
为true
,将flushWork
传递给requestHostCallback
-
advanceTimers
逻辑:接收一个参数currentTime
,检查timerQueue
中的任务,将不再延时的任务添加到taskQueue
中。将timerQueue
中的堆顶任务弹出,如果不存在timer.callback
,任务取消,并且弹出timerQueue
,如果timer.startTime <= currentTime
,任务弹出timerQueue
,并且添加到taskQueue
中. -
flushWork
逻辑:接收(hasTimeRemaining, initialTime)
两个参数,isHostCallbackScheduled
设置为false
,如果isHostTimeoutScheduled
,设置为false
,取消定时器,然后执行workLoop(hasTimeRemaining, initialTime)
-
workLoop
逻辑:接收(hasTimeRemaining, initialTime)
两个参数,当前任务不为空或任务不停止的情况下,执行循环。当当前任务还没有过期,但是到了deadline
,则跳出循环;currentTask
有回调的情况下,执行回调,currentTask
等于栈顶元素的情况下,将任务从taskQueue
中弹出,执行advanceTimers
将不再延时的任务添加到任务队列;currentTask
没有回调的情况下,将任务从taskQueue
中弹出。currentTask
为空的情况下,获取timerQueue
的栈顶任务,放入requestHostTimeout
中。
-
https://someu.github.io/2020-11-10/react-scheduler%E6%BA%90%E7%A0%81%E8%A7%A3%E6%9E%90/
https://juejin.cn/post/6889314677528985614
https://juejin.cn/post/6914089940649246734
md格式的文件直接粘过来有点丑,待补充
网友评论