欢迎来到尧图网

客户服务 关于我们

您的位置:首页 > 文旅 > 美景 > React源码揭秘 | scheduler 并发更新原理

React源码揭秘 | scheduler 并发更新原理

2025/2/14 1:04:09 来源:https://blog.csdn.net/weixin_40710412/article/details/145590841  浏览:    关键词:React源码揭秘 | scheduler 并发更新原理

React 18增加了并发更新特性,开发者可以通过useTransition等hooks延迟执行优先级较低的更新任务,以达到页面平滑切换,不阻塞用户时间的目的。其实现正是依靠scheduler库。

scheduler是一个依赖时间片分片的任务调度器,React团队将其单独发包,你可以通过以下指令来单独使用。

npm install scheduler 

scheduler - npm

在CPU密集操作中(如大量echarts数据补点等)可以借助scheduler将大的任务拆分成子任务运行,并且不阻塞渲染引擎以及用户事件。

为了了解scheduler 我自己实现了一个my-scheduler库,精简了scheduler的实现并且增加了详细的注释,下面的讲解将依赖  my-sample-scheduler - npm 

项目地址: https://github.com/Gravity2333/my-scheduler

基本使用

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赋值,结束workloopcurrentTask.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;// 先检查一下有没有延迟任务需要加入到taskQueuethis.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赋值,结束workloopcurrentTask.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结束,需要开启延迟任务,以确保在延迟到达时,能启动messageLoopconst 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 < currentTimeconst currentTime = performance.now();if (currentTimerTask.expirationTime < currentTime) {// 弹出当前任务this.timerQueue.pop();// 设置sortIndexcurrentTimerTask.sortIndex = currentTimerTask.expirationTime;// 加入taskQueuethis.taskQueue.push(currentTimerTask);} else {// 如果没有超时 由于peek拿到的是最快过期的任务,所以后面的不用检查了 直接returnreturn;}}// 处理下一个timerTaskcurrentTimerTask = 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库的简单介绍和总结

版权声明:

本网仅为发布的内容提供存储空间,不对发表、转载的内容提供任何形式的保证。凡本网注明“来源:XXX网络”的作品,均转载自其它媒体,著作权归作者所有,商业转载请联系作者获得授权,非商业转载请注明出处。

我们尊重并感谢每一位作者,均已注明文章来源和作者。如因作品内容、版权或其它问题,请及时与我们联系,联系邮箱:809451989@qq.com,投稿邮箱:809451989@qq.com