React 18增加了并发更新特性,开发者可以通过useTransition等hooks延迟执行优先级较低的更新任务,以达到页面平滑切换,不阻塞用户时间的目的。其实现正是依靠scheduler库。
scheduler是一个依赖时间片分片的任务调度器,React团队将其单独发包,你可以通过以下指令来单独使用。
npm install scheduler
在CPU密集操作中(如大量echarts数据补点等)可以借助scheduler将大的任务拆分成子任务运行,并且不阻塞渲染引擎以及用户事件。
为了了解scheduler 我自己实现了一个my-scheduler库,精简了scheduler的实现并且增加了详细的注释,下面的讲解将依赖 my-sample-scheduler - npm
基本使用
scheduler库的入口是scheduleCallback,这个函数的定义如下
@param priority — 用户回调函数优先级
@param callback — 用户回调函数
@param delay — 可选,配置延迟任务 单位毫秒
scheduleCallback(
priorityLevel?: PriorityLevel,
callback?: UserCallback,
delay?: number): UserCallbackTask
其中,PriorityLevel为scheduler定义的优先级,其包含以下五种,优先级从高到低
IMMEDIATE_PRIORITY:立即执行任务,具有最高优先级。适用于需要立即执行的任务。
USER_BLOCKING_PRIORITY:用户阻塞任务,适用于需要交互的任务。NORMAL_PRIORITY:普通任务,默认优先级。
LOW_PRIORITY:低优先级任务,适用于不急需执行的任务。
IDLE_PRIORITY:空闲任务,只有在没有其他任务时才会执行。
callback为需要执行的回调,接受一个didTimeout: boolean参数,表示当前执行的任务是否超时,开发者需要决定是否继续执行任务,还是拆分成更小的任务下次执行。
delay为延迟时间,传入之后表示当前任务为延迟任务,会在延迟时间结束之后接受调度并运行。
以下为简单使用DEMO:
import scheduler,{ PriorityLevel } from "my-sample-scheduler";
// 简单的任务调度
scheduler.scheduleCallback(
PriorityLevel.NORMAL_PRIORITY,
(didUserCallbackTimeout) => {
console.log("任务开始执行", didUserCallbackTimeout);
return; // 任务执行完毕
}
);
// 次任务会立刻调度
// 延迟任务调度
scheduler.scheduleCallback(
PriorityLevel.LOW_PRIORITY,
(didUserCallbackTimeout) => {
console.log('低优先级任务执行', didUserCallbackTimeout);
return; // 任务执行完毕
},
1000 // 延迟1秒
);
// 此任务会在1s后被调度执行
再看一个拆分大任务的例子
// 生成随机的时间和数据
const generateData = (cnt: number) => {
const data = [];
for (let i = 0; i < cnt; i++) { // 每次生成1个数据点
const value = Math.random() * 10; // 随机生成一个值
data.push(value);
}
return data;
};
let generatedCnt = 0
const generateHugeData: UserCallback = (didUserCallbackTimeout) => {
if(generatedCnt > 10000) return // 超过10000 直接return 表示任务结束
// 本次任务需要生成的点,如果没超时一次生成1000个 如果超时,则一口气生成剩下的所有点
const currentGenerateCnt = didUserCallbackTimeout? 10000-generatedCnt : 1000
// 生成点 耗时任务
const newData = generateData(currentGenerateCnt);
// 设置点
setHugeData((prev) => [...prev, ...newData]);
// 设置总共生成的点数
generatedCnt += currentGenerateCnt
// 返回completePoint 表示任务还没执行完 有剩余任务
return completePoint;
};
// 注册scheduler事件
scheduler.scheduleCallback(PriorityLevel.IDLE_PRIORITY, generateHugeData);
比如当前我要随机生成10000个点, generateData是耗时任务,如果一次性同步生成10000个点,会导致浏览器卡住,阻塞浏览器渲染,但是通过scheduler,我们可以将其拆分成10次任务,每生成1000个点之后,就将剩下生成点的任务封装成子任务返回,并且让出主线程,这样就不会阻塞页面渲染。
实现原理
我们知道,浏览器只提供一个主线程给Javascript解析引擎和渲染引擎,这两个引擎通过事件循环交替工作。scheduler的实现就是参考了操作系统多任务中的 时间片分片策略。
一般浏览器渲染帧率为60HZ 也就是1s内,渲染引擎需要工作60次,算下来大概16.7MS 就要保证渲染引擎工作一次,否则就会造成渲染延迟,用户页面卡顿。
由于js解析引擎和渲染引擎的互斥性,并且一旦js引擎解析运行同步代码,就不能被打断,所以过多的CPU密集运算(如上面的补10000个点)就会造成js解析引擎过多占用主线程,导致渲染引擎无法工作,也无法响应用户事件!
scheduler的作用就是对任务进行拆分,并且把每个小任务通过宏任务运行(你可以先理解为放到settimeout中)当js解析引擎在一个循环中处理完同步代码,微任务代码后,会检查是否还有时间来处理这些在宏任务中的子任务。如果有则运行,如果没有就把主线程让给渲染引擎,在下一轮循环中,在找时机运行!
我们运行上面生成数据的例子(你可以下载my-scheduler项目,使用yarn start查看DEMO),通过performance可以观察到,每个任务都被分配给大概6ms左右的时间片来运行,当一个任务运行结束后,如果当前帧还有执行剩余任务的时间,就继续执行下一个任务,如果没有就让出主线程!
执行单元&大小任务
上面我所用的任务可能不严谨,其实应该是成为执行单元,scheduler中定义了一个变量
const frameMS = 5
这个变量定义了每个执行单元所占用时间片的时间,一个执行单元开始运行的时候,会重置一个全局的startTime,表示本执行单元运行的开始时间,一般使用performance.now() 获取高精度时间戳。
一个执行单元可能运行多个任务,运行任务的数量取决于当前任务是 "大任务" 还是 “小任务”
大或是小,不由任务运行时常来决定,因为调度器在任务运行结束之前,是无法预测到任务的规模,所以scheduler将任务大小的划分,交给开发者决定
当传入scheduleCallback的回调函数返回一个函数时,scheduler默认其还有剩下没执行完的子任务,不管当前执行单元有没有用完当前5ms的时间切片,都结束当前的执行单元运行,在下一次执行单元被调度时继续运行。
当返回值不是个函数,那么默认其为小任务,一个执行单元只能运行一个大任务,但是可以运行多个小任务。这样设计的原因是,如果任务过小,那么和每个执行单元的调度,创建,运行的上下文代码所产生的开销相比起来,就不那么划算,合并到一个执行单元中运行能提升效率。简易时间轴如下:
以下为源码中workLoop函数中的部分,可以看到上述实现
// workLoop
/** 还有时间 执行callback */
const callback = currentTask.callback;
if (typeof callback === "function") {
// callback置空
currentTask.callback = null;
// 运行callback 获得返回值
const continuationCallback = callback(isUserCallbackTimeout);、
if (typeof continuationCallback === "function") {
// 如果返回了剩余任务,表示当前执行的是大任务,重新给task的callback赋值,结束workloop
currentTask.callback = continuationCallback;
// 表示还有任务
return true;
} else {
// 当前任务执行完了,小任务,继续while执行
...
}
} else {
// 如果callback为空 或者不是函数,说明当前任务不可执行 也可能是当前任务已经报错了,直接弹出
this.taskQueue.pop();
}
// 此时 继续循环执行小任务 取下一个任务
currentTask = this.taskQueue.peek();
如何实现优先级调度
如果你只需要实现大任务拆分小任务,使用settimeout也可以简单实现,scheduler的核心在于实现了一套基于过期时间的优先级算法。
scheduler支持五种优先级,优先级大小从高到低,但是这五种优先级是如何落实到调度上的呢?
scheduler采用了过期时间来调度任务,其核心理念是,每次都找到最快过期的任务并且运行,而五种优先级,对应的就是五种timeout
/** 调度优先级 */
export enum PriorityLevel {
/** 立即执行优先级 优先级最高 */
"IMMEDIATE_PRIORITY" = "IMMEDIATE_PRIORITY",
/** 用户阻塞优先级 此之 */
"USER_BLOCKING_PRIORITY" = "USER_BLOCKING_PRIORITY",
/** 正常默认优先级 */
"NORMAL_PRIORITY" = "NORMAL_PRIORITY",
/** 低优先级 */
"LOW_PRIORITY" = "LOW_PRIORITY",
/** IDLE 优先级 优先级最低 等待时间无限长 */
"IDLE_PRIORITY" = "IDLE_PRIORITY",
}
/** 优先级到超时时间的映射 */
const PRIORITY_LEVEL_TO_TIMEOUT_MAP: Record<`${PriorityLevel}`, number> = {
[PriorityLevel.IMMEDIATE_PRIORITY]: -1,
[PriorityLevel.USER_BLOCKING_PRIORITY]: 250,
[PriorityLevel.NORMAL_PRIORITY]: 500,
[PriorityLevel.LOW_PRIORITY]: 1000,
[PriorityLevel.IDLE_PRIORITY]: Number.MAX_SAFE_INTEGER,
};
可以看到,优先级越高,其对应的过期时间越短。也就是说,越急迫的任务就需要被更快的调度,每当scheduleCallback一个任务时,其内部就会通过PRIORITY_LEVEL_TO_TIMEOUT_MAP 把优先级转换成timeout。
如何避免低优先级任务"饿死"
考虑一种情况,如果当前scheduler中存在两个优先级任务,一个优先级很高,一个优先级很低,那么如果高优先级任务的代码运行中,一直往scheduler中注册高优先级任务,那么低优先级任务就一直得不到运行,导致任务 “饿死”
解决的办法就是引入startTime,并且计算出一个最终的expreationTime 其计算方法如下:
expirationTime = startTime + timeout
对于低优先级任务,虽然其expirationTime很大,但是对于后续加入进来的任务,由于startTime是递增的,所以长时间等待的任务优先级就会越来越高,expreationTime也相对越来越小,这就解决了任务饿死的问题。其在scheduleCallback中的实现如下:
public scheduleCallback(
priorityLevel: PriorityLevel = PriorityLevel.NORMAL_PRIORITY,
callback: UserCallback = () => {},
delay = 0
) {
/** 获取当前高精度时间 */
const currentTime = performance.now();
/** 任务开始时间 */
const startTime = currentTime + delay;
/** 根据优先级,计算timeout
* 默认为NORMAL 即 500
*/
const timeout =
PRIORITY_LEVEL_TO_TIMEOUT_MAP[priorityLevel] ||
PRIORITY_LEVEL_TO_TIMEOUT_MAP[PriorityLevel.NORMAL_PRIORITY];
/** 过期时间
* 对于普通任务 currentTime + timeout
* 对于延迟任务 currentTime + delay + timeout
*/
const expirationTime = startTime + timeout;
/** 把callback封装成UserCallbackTask */
const userCallbackTask: UserCallbackTask = {
id: this.userTaskCnt++,
priorityLevel,
startTime,
expirationTime,
callback,
sortIndex: -1,
};
/** 普通任务使用expirationTime 作为sortIndex调度 sortIndex可以理解为expreTime*/
userCallbackTask.sortIndex = expirationTime;
/** 加入taskQueue */
this.taskQueue.push(userCallbackTask);
// 调度任务
return userCallbackTask;
}
优先级排序&小顶堆
scheduler每次都会选择过期时间最小的任务运行,那么如何找到最小任务呢,最简单的做法是每次调度时都对任务根据优先级排序,但是排序算法的复杂度较高,一般为O(n^2)级别。并且,我们每次只需要找到最小的那个值,把其余的值都进行排序很浪费时间。
所以,scheduler维护了一棵小顶堆来获取最快过期的任务,小顶堆的好处在于,其插入,删除的复杂度为O(logn) 取得最小值的操作复杂度为O(1) 由于最开始没有任何任务,所以不需要建堆的过程,只需要每次插入,删除数据时,对其进行上下移来维护最小值即可。
以小顶堆为基础的任务队列在libs/mini-heap.ts中实现,其中主要方法是
MiniHeap.pop 删除节点
MiniHeap.push 插入节点
MiniHeap.peak 获取顶峰 优先级最高的节点
我们在scheduleCallback时,会将回调函数封装成任务,存入MiniHeap,并且根据过期时间维护最早过期任务。
scheduler中维护了两个任务队列 taskQueue和timerQueue 分别负责记录同步任务和延迟任务
/** 比较函数 比较两个任务的优先级 */
const compare = (a: UserCallbackTask, b: UserCallbackTask) => {
const diff = a.expirationTime - b.expirationTime;
return diff !== 0 ? diff : a.id - b.id;
};
/** 声明任务队列 */
private taskQueue: MiniHeap<UserCallbackTask> = new MiniHeap(compare);
/** 声明延迟队列 */
private timerQueue: MiniHeap<UserCallbackTask> = new MiniHeap(compare);
当我们scheduleCallback一个任务的时候,会根据delay参数是否传递,判断其是否为延迟任务,delay参数会被加入到StartTime中。
/** 获取当前高精度时间 */
const currentTime = performance.now();
/** 任务开始时间
* 如果非延迟 就是currentTime
* 如果配置了delay 则startTime = currentTime + delay
*/
const startTime = currentTime + delay;
/** 根据优先级,计算timeout
* 默认为NORMAL 即 500
*/
const timeout =
PRIORITY_LEVEL_TO_TIMEOUT_MAP[priorityLevel] ||
PRIORITY_LEVEL_TO_TIMEOUT_MAP[PriorityLevel.NORMAL_PRIORITY];
/** 过期时间
* 对于普通任务 currentTime + timeout
* 对于延迟任务 currentTime + delay + timeout
*/
const expirationTime = startTime + timeout;
if (startTime > currentTime) {
/** 如果是延迟任务, 用startTime来用作sortIndex排序调度 当达到开始时间后,转移到taskQueue */
userCallbackTask.sortIndex = startTime;
/** 加入延迟队列 */
this.timerQueue.push(userCallbackTask);
} else {
/** 如果是普通任务 普通任务使用expirationTime 作为sortIndex调度 */
userCallbackTask.sortIndex = expirationTime;
/** 加入taskQueue */
this.taskQueue.push(userCallbackTask);
}
如果是延迟任务,其startTime > currentTime 此时task.sortIndex = startTime 也就是,当到达delay时间之后,才开始同步调度,并且加入到timerQueue中
如果不是,则把expirationTime作为sortIndex 并且加入到taskQueue中。
scheduleCallback主要流程如下:
开启调度&MessageChannel
我们知道,scheduler的任务是作为宏任务运行的,如何创建宏任务?
你也许知道,requestIdleCallback 其会在浏览器空闲时间运行宏任务,但是这个API的兼容性不好
你也许会说setTimeout(callback,0) 没错,这样确实可以创建宏任务,但是setTimeout的问题在于,其delay参数即使不填,也会有4ms左右的延迟时间,即setTimeout(callback,4) 这段时间就会被浪费掉。
scheduler选了一种兼容性相对好,并且没有4ms延迟时间的创建宏任务方式,即MessageChannel
MessageChannel是一个构造方法,可以创建一个消息通道,其实例包含两个端口 port1 port2
在其中一段onmessage帮定事件,在另一端postMessage,其回调函数会在同步代码 微任务代码执行之后 立刻执行! 其使用方式如下:
const messageChannel = new MessageChannel()
messageChannel.port2.onmessage = ()=>{
console.log('立刻执行')
}
messageChannel.port1.postmessage(null)
scheduler中,实现该逻辑的为 performWorkUntilDeadline 函数,为了兼容没有MessageChannel的环境,其用setTimeout做了兜底
/** 调度任务 使用messageChannel
* messageChannel的好处是
* 1. 可以创建宏任务 不阻塞主线程
* 2. 相比于settimeout 延迟更小
* 3. 在没有messageChannel的情况下,使用settimeout兜底
*/
private schedulePerformWorkUntilDeadline() {
if (typeof MessageChannel === "function") {
const messageChannel = new MessageChannel();
messageChannel.port2.onmessage = this.performWorkUntilDeadline.bind(this);
/** 发送消息 */
messageChannel.port1.postMessage(null);
} else {
/* 没有MessageChannel 用settimeout兜底*/
setTimeout(() => {
this.performWorkUntilDeadline();
});
}
}
schedulePerformWorkUntilDeadline由requestHostCallback调用,这个命名应该是参考了requestIdleCallback
/** 开启任务循环 */
private requestHostCallback() {
/** 在这里开启循环,并且上锁,保证只有一个performWorkUntilDeadline在运行 */
if (!this.isMessageLoopRunning) {
this.isMessageLoopRunning = true;
this.schedulePerformWorkUntilDeadline();
}
}
requestHostCallback的作用是,检查isMessageLoopRuning锁是否可进入,如果可以则上锁并且开启调度,isMessageLoopRunning 表示消息循环是否在运行。如果当前消息循环在运行, 加入到任务队列中的任务一定能得到运行,就不必重复调度了。
消息循环MessageLoop
消息循环由requestHostCallback开启,直到taskQueue中没有任何任务停止,其标识为isMessageLoopRunning 示意图如下:
其中包含了 schdulePerformWorkUntilDeadline performWorkUntilDeadline flushWork workLoop
performWorkUntilDeadline
performWorkUntilDeadline 就是执行一个任务单元,其时间片长度为frameMS=5ms 其实现如下:
/** 持续循环运行任务
* 开启一个时间切片的任务,时间切片的宽度为frameYieldMs 默认5ms
* 每次时间切片运行结束后,如果还有任务,重复调用performWorkUntilDeadline继续运行
* 没有任务了,则释放isMessageLoopRunning锁,循环停止运行
*/
private performWorkUntilDeadline() {
if (this.isMessageLoopRunning) {
/** 获得每次循环的开始时间 */
const workStartTime = performance.now();
this.startTime = workStartTime;
/**
* 解释一下,这里为什么用try..finally
* try中调用flushWork 执行任务,每次执行任务时,会从taskQueue中peek一个任务运行
* peek出来之后,会先把task.callback保存到一个临时变量callback中,并且给 task.callback 赋 null
* 判断这个临时的callback 如果是function 则运行,运行之后如果还有没运行完的任务 再给task.callback = remainingTaskFunc
* 如果callback 不存在 或者不是函数 不可运行 则直接弹出这个任务
*
* 如果callback执行内部报错,那么此时 task.callback = null 并且跳出flushWork 这里的做法是,如果有错误则忽略掉,通过finally继续开启下一个performWorkUntilDeadline
* 当下一个performWorkUntilDeadline开启后,由于task.callback = null 会直接pop出taskQueue 做到了忽略错误继续运行loop
*/
let hasMorkWork = true;
try {
hasMorkWork = this.flushWork(workStartTime);
} finally {
if (hasMorkWork) {
/** 还有任务 继续运行 */
this.schedulePerformWorkUntilDeadline();
} else {
/** 没有任务了 关闭loop */
this.isMessageLoopRunning = false;
}
}
}
}
其作用就是调用flushWork,开始一个任务单元,并且在flushWork执行结束后,判断是否还有剩余任务,如果有则再次通过schedulePerformWorkUntilDeadline重新调度任务单元执行。
这里需要注意,hasMorework默认值为true,为了是保证当flushWork中出现异常时,能继续调度运行任务单元,尝试“跳过”错误,后面会详细说
如果没有剩余任务 则关闭MessageLoop循环
任务开始之前需要重置全局的startTime,用来后面的shouldYiledToHost计算
flushWork
flushWork的主要作用是,给isPerformingWork上锁,表示任务单元正在运行,保留上下文优先级,执行workLoop,并且在workLoop执行结束之后,恢复isPerformingwork和优先级
/**
* flushWork 运行任务 一个5ms的时间 并且返回是否还有任务
* @param workStartTime
* @returns
*/
private flushWork(workStartTime: number): boolean {
/** flushWork 的作用是
* 1. 调用workloop 并且保证workloop是临界资源,对其加锁
* 2. 如果有延迟任务在运行,则取消掉,因为延迟任务没意义了
* (延迟任务就是为了在延迟到达的时候把任务放到taskQueue 并且开启loop 在当前任务执行完之前 延迟任务即使到达了 也只能等着,在每次workLoop的小循环运行结束和workLoop运行结束 都会advaned)
* 3.任务开始了 代表 hostCallbackSchedule的调度过程(loop触发过程)已经结束 释放锁
* 4. 使用try..finally 忽略错误,在finally中释放isPerformWork
*
*/
this.isHostCallbackScheduled = false;
// 定时任务没必要和messageLoop一起运行,这里取消掉定时器
this.cancelHostTimeout();
// 加锁
this.isPerformingWork = true;
const previousPriorityLeve = this.currentPriorityLevel;
try {
return this.workLoop(workStartTime);
} finally {
/** 注意 这里finally一定会在最后执行 即便上面有return (return只是标记了返回点) */
this.isPerformingWork = false;
/** 恢复优先级 */
this.currentPriorityLevel = previousPriorityLeve;
}
}
workLoop
workLoop是整个messageLoop的核心
第一步,获取当前的时间,并且优先通过advanceTimers 检查timerQueue,把延迟已经结束的任务放到taskQueue中去
获取当前taskQueue中优先级最高的任务
let workCurrentTime = workStartTime;
// 先检查一下有没有延迟任务需要加入到taskQueue
this.advanceTimers();
// 取得优先级最高的任务
let currentTask = this.taskQueue.peek();
第二步,取得任务,开始循环
首先需要判断 当前取出来的任务是否超时,也就是判断当前的时间 是不是在任务的过期时间之后,这个参数后面会传递给callback,由开发者决定到底要继续拆分任务,还是一口气执行完
如果没超时,需要判断此时是否超过了当前任务单元的时间片,即5ms,判断的逻辑由shouldYieldToHost提供 (注意,前面performWorkUntilDeadline中设置的全局startTime就是这里用的,判断是否超出时间片时间)
/** 是否应当让出主线程 */
public shouldYieldToHost(): boolean {
const timeElapsed = performance.now() - this.startTime;
if (timeElapsed < frameYieldMs) {
// The main thread has only been blocked for a really short amount of time;
// smaller than a single frame. Don't yield yet.
return false;
}
// Yield now.
return true;
}
后面就是执行callback了,根据返回值判断是否为大小任务,前面说过 不赘述了
let isUserCallbackTimeout = false;
while (currentTask) {
// 更新判断是否超时
isUserCallbackTimeout = currentTask.expirationTime < workCurrentTime;
if (!isUserCallbackTimeout && this.shouldYieldToHost()) {
// 让出主线程
break;
}
/** 还有时间 执行callback */
const callback = currentTask.callback;
if (typeof callback === "function") {
// callback置空
currentTask.callback = null;
// 更新优先级
this.currentPriorityLevel = currentTask.priorityLevel;
// 保证callback可调用
const continuationCallback = callback(isUserCallbackTimeout);
if (typeof continuationCallback === "function") {
// 如果返回了剩余任务,表示当前执行的是大任务,重新给task的callback赋值,结束workloop
currentTask.callback = continuationCallback;
// 看一下是否有可以加入到taskQueue的延迟任务
this.advanceTimers();
// 表示还有任务
return true;
} else {
// 当前任务执行完了,小任务,继续while执行
if (currentTask === this.taskQueue.peek()) {
this.taskQueue.pop(); // 弹出当前执行完的任务
}
// 看一下是否有可以加入到taskQueue的延迟任务
this.advanceTimers();
}
} else {
// 如果callback为空 或者不是函数,说明当前任务不可执行 也可能是当前任务已经报错了,直接弹出
this.taskQueue.pop();
}
// 此时 继续循环执行小任务 取下一个任务
currentTask = this.taskQueue.peek();
}
这里面有个操作是,当使用taskQueue.peak() 拿到优先级最高的任务后,保存callback并且把任务的callback置为null ,如果当前任务还有子任务,就把continuationCallback再赋回去,这样做的好处是,如果callback抛出异常,那么此时workloop,flushwork都会停止运行,但是由于perfromWorkUntilDeadline中的hasMoreWork默认值为true,保证在抛出错误后还能继续运行MessageLoop,那么在下次运行到workLoop时,由于callback为空,会直接弹出,保证了在出现错误时还能正常运行其他任务!
执行单元运行结束后,需要判断后面还有没有要执行的任务,如果没有需要再次启动计时器,保证延迟任务被正确调度
// 执行到这里,1.可能是是currentTask没超时,但是没有时间片了,推出workLoop,返回true表示还是任务 2.可能是没任务了
if (currentTask) {
return true;
} else {
// taskQueue没有任务了,此时返回false,此时flushwork结束,messageLoop结束,需要开启延迟任务,以确保在延迟到达时,能启动messageLoop
const timerTask = this.timerQueue.peek();
if (timerTask) {
// 检查timerQueue 有任务则开启
const firstTimer = this.timerQueue.peek();
if (firstTimer) {
this.requestHostTimeout(
this.handleTimeout,
firstTimer.startTime - performance.now()
);
}
}
return false;
}
MessageLoop的主要流程如下:
延迟任务的处理
延迟任务的处理主要就是开启定时器,在定时器结束之后,调用advanedTimers,把延迟任务加入到taskQueue中调度,其实现如下:
/** 这个函数的作用是,检查延迟队列,如果有已经完成延迟的 则加入任务队列 */
private advanceTimers() {
let currentTimerTask = this.timerQueue.peek();
while (currentTimerTask) {
// 判断,任务的callback是否为函数 如果不是 无法执行 直接弹出
if (typeof currentTimerTask.callback !== "function") {
this.taskQueue.pop();
} else {
// 检查延迟是否结束 判断条件是 task.expireTime < currentTime
const currentTime = performance.now();
if (currentTimerTask.expirationTime < currentTime) {
// 弹出当前任务
this.timerQueue.pop();
// 设置sortIndex
currentTimerTask.sortIndex = currentTimerTask.expirationTime;
// 加入taskQueue
this.taskQueue.push(currentTimerTask);
} else {
// 如果没有超时 由于peek拿到的是最快过期的任务,所以后面的不用检查了 直接return
return;
}
}
// 处理下一个timerTask
currentTimerTask = this.timerQueue.peek();
}
}
如何结合React
我们知道,React和scheduler有不同的优先级系统。在React中,通常需要把lane优先级转换成scheuler优先级并且调用scheduleCallback 这就需要React内部的lanesToSchedulerPriority
同时,当我们发起更新时,也需要获取scheduler内部正在运行的优先级,这就需要scheduler提供的getCurrentPriorityLevel方法
/** 获取当前的优先级 */
getCurrentPriorityLevel() {
return this.currentPriorityLevel;
}
React中fiberLane.ts下的requestUpdateLane就是调用这个方法获取scheduler优先级,并且通过schedulerPriorityToLane转换成lane优先级
事件处理
React的不同事件对应不同的优先级,其内部维护了一个事件优先级到scheduler优先级的转换函数来完成这个转换过程。
function eventTypeToSchedulerPriority(eventType: string) {
switch (eventType) {
case "click":
...
return PriorityLevel.IMMEDIATE_PRIORITY;
case "scroll":
case "touchend":
...
return PriorityLevel.USER_BLOCKING_PRIORITY;
case "input":
...
return PriorityLevel.NORMAL_PRIORITY;
case "abort":
case "load":
...
return PriorityLevel.LOW_PRIORITY;
default:
return PriorityLevel.IDLE_PRIORITY;
}
}
同时,为了保证事件内的hooks setter 如setXXX 能正确获得当前的优先级,其调用scheduler中的runWithPriority,其作用就是修改当前全局优先级,并且运行事件处理代码
/** 以某优先级运行 */
runWithPriority(priorityLevel: PriorityLevel, callback: any) {
const priviouseLevel = this.currentPriorityLevel;
this.currentPriorityLevel = priorityLevel;
try {
callback();
} catch (e) {
console.warn(e);
} finally {
this.currentPriorityLevel = priviouseLevel;
}
}
在callback内部,如果存在setXXX hooks setter,其内部的dispatchSetState ,requestUpdateLane就会获取到当前的全局优先级!
以上就是scheduler库的简单介绍和总结