一、引言
(1)Scheduler 是什么
Scheduler 是 React 团队开发的一个用于事务调度的包,内置于 React 项目中。其团队的愿景是孵化完成后,使这个包独立于 React,成为一个能有更广泛使用的工具。
npm包地址:scheduler - npm
github地址:react/packages/scheduler/src at main · facebook/react
Scheduler 是一个任务调度器,它会根据任务的优先级对任务进行调用执行。 在有多个任务的情况下,它会先执行优先级高的任务。如果一个任务执行的时间过长,Scheduler 会中断当前任务,让出线程的执行权,避免造成用户操作时界面的卡顿。在下一次恢复未完成的任务的执行。
(2)为什么需要Schedule
【想象一下】:
你们公司就一个厕所,方圆几万公里找不到另外任何一个厕所了 (🌿哪个煞福设计的?)
现在,公司有100万个员工要上厕所 (你内心os:完了,裤子不保了😱)
如果没有调度器协调管理会发生什么?
-
张三:憋不住了!我要立即上!(用户点击)
-
李四:我有个大需求要做,得蹲久一点(大数据计算)
-
王五:我摸鱼,不急(后台同步)
结果:张三在门口憋得脸都绿了,李四在里面刷抖音不出来,王五还在慢悠悠排队...
这就是没有调度器的前端应用:用户交互卡成🐕,用户气炸了(哪个 迪奥🐱 做的网页,这是把ppt端上来了?)
React Scheduler就是这个管理员,他决定了任务的优先级、时间切片、以及啥时候让出主线程等等,下面我们从0开始实现这个调度过程(放心,真的是从零开始的😊)
(3)项目架构
bash
Scheduler/
├── 1.Priorities.js # 优先级定义 - 定义5个优先级常量
├── 2.MinHeap.js # 最小堆实现 - 任务队列的数据结构
├── 3.TimeTools.js # 时间工具 - 时间切片与时间管理
├── 4.SchedulerState.js # 调度器状态管理 - 状态变量封装
├── 5.ShouldYieldToHost.js # 时间切片判断 - 判断是否需要让出主线程
├── 6.AdvanceTimers.js # 延迟任务处理 - timerQueue的处理机制
├── 7.WorkLoop.js # 工作循环核心 - 任务执行主循环
├── 8.PerformWorkUntilDeadline.js # 任务执行器 - 实现时间切片的工作调度
├── 9.ScheduleCallback.js # 核心调度API - 安排任务
├── 10.CancelAndQuery.js # 取消和查询API - 任务取消和状态查询
├── 11.PriorityControl.js # 优先级控制 - 优先级切换和嵌套
├── 12.PaintUtils.js # 绘制工具 - 浏览器绘制相关
├── 13.WrapCallback.js # 回调包装 - 回调函数包装和优先级继承
├── 14.Scheduler.js # 调度器入口 - 导出所有公共API
tips:你别看这文件有这么多,但是核心思想没那么难,实际上代码可一点也不少🤣
二、完整代码
(1)优先级
还是刚刚那个例子,你恰是100万幸运儿中排到第一个的人,马上就要轮到你去使用厕所了,马上要憋不住了,就在这千钧一发之际,领导来了,你看他憋得发绿得脸,突然想到这个月月底,你马上就要升职加薪了,然而这一切都归终于领导的意见,在这种情况下,他想插个队,你让还是不让? (你:领导您请,我不急😁 在某一方小天地:我*@#称冯了个福,你个迪奥🐱,偏偏我最急的时候来!要不是升职还要看你的脸色,我直接把厕所占了,外面投放核弹我都不开门!)
问题:怎么知道谁更急?总不能让大家比谁跳得高吧?
**答案:**Schedule给每个任务分配了优先级,目的:让紧急任务先执行,避免页面卡死
javascript
// 1.Priorities.js - 对应官方github仓库下的packages/scheduler/src/SchedulerPriorities.js文件
export const ImmediatePriority = 1; // 最高优先级:需要立即执行的任务 救命!要拉裤子里了!
export const UserBlockingPriority = 2; // 用户阻塞优先级:用户交互相关任务 快点,我憋不住了!
export const NormalPriority = 3; // 普通优先级:常规更新任务 不急,但也要上
export const LowPriority = 4; // 低优先级:可延迟执行的任务 等没人时候再去
export const IdlePriority = 5; // 空闲优先级:在空闲时执行的任务 等下班再说
(2)最小堆
问题:100万人要上厕所,怎么快速找到最急的那个? 总不能让大家打一架吧,打架的时候一用力,万一......是吧🤣?
煞福方案 :sort() → O(n log n) 还没排完,就拉裤子里了!
聪明方案:最小堆 → O(log n) 快速找到最急的人!
javascript
// 02.MinHeap.js - 对应官方github仓库下的packages/scheduler/src/SchedulerMinHeap.js文件
/** 用数组模拟二叉树,总让最急的人排前面 */
// (1)比谁更急 先比过期时间,一样急就比谁先来的
function compare(a, b) {
const diff = a.sortIndex - b.sortIndex;
return diff === 0 ? a.id - b.id : diff;
}
// (2)新人排队
export function push(heap, node) {
heap.push(node); // 新来的站队尾
siftUp(heap,node,i) // 向上调整 -- 具体代码暂不列出,文章末尾将附上项目地址,包含完整代码示例
}
// (3)瞄一眼队首
export function peek(heap) return heap.length === 0 ? null : heap[0];
// (4)队首的人上厕所
export function pop(heap) {
if (heap.length === 0) return null;
const first = heap[0]; // 最急的人
const last = heap.pop(); // 最后一个人
if (last !== first) {
heap[0] = last; // 最后一个人补到第一个位置
siftDown(heap,node,i) // 向下调整
}
return first;
}
(3)Time工具
问题 :怎么知道一个人到底能憋多久,用什么计时?
回答:给每个人一个"憋尿计时器"!
javascript
// 03.TimeTools.js
// (1)获取当前精确时间
export function unstable_now() {
// 优先用performance.now(),更精确!
if (typeof performance === 'object' && typeof performance.now === 'function') return performance.now();
// 老浏览器用Date.now()凑合
return Date.now();
}
// (2)根据尿急等级计算能憋多久
export function getTimeoutByPriority(priorityLevel) {
/**
* ImmediatePriority - 董事长:想上就上,永不过期!
* UserBlockingPriority - 急尿员工:250ms后就要拉裤子里了
* NormalPriority - 普通员工:5秒后就要拉裤子里了
* LowPriority - 摸鱼员工:10秒后就要拉裤子里了
* IdlePriority - 佛系员工:基本上能忍到天荒地老
*/
switch (priorityLevel) {
case 1: return -1;
case 2: return 250;
case 3: return 5000;
case 4: return 10000;
case 5: return 1073741823; // 最大整数值
default: return 5000;
}
}
(4)全局状态对象
javascript
// 4. SchedulerState.js
/**
* 核心职责:统一管理调度器运行过程中的所有重要状态
* 设计理念:集中管理,避免状态分散导致的逻辑混乱
*/
// ==================== 状态对象 ====================
export const SCHEDULER_STATE = {
// 队列管理 tips:这俩在其他地方也可能被称为:任务池
taskQueue: [], // 立即执行任务队列
timerQueue: [], // 延迟执行任务队列
// 任务管理
taskIdCounter: 1, // 任务ID计数器
currentTask: null, // 当前正在执行的任务对象
currentPriorityLevel: 3, // 当前调度器优先级
// 调度状态标志
isPerformingWork: false, // 是否正在执行工作循环
isHostCallbackScheduled: false, // 是否已安排工作循环调度
isHostTimeoutScheduled: false, // 是否已安排延迟任务检查
// 时间管理
deadline: 0, // 当前时间片的截止时间
startTime: -1, // 当前工作循环的开始时间
needsPaint: false, // 是否需要浏览器重绘
taskTimeoutID: -1, // 延迟任务检查定时器的ID
// 时间切片配置
/*
* (你的内心:完了完了,时间切片,听起来好牛逼啊,肯定又是什么我学不会的复杂算法吧...学你m🤬)
* 💡别慌! <<世界就是个巨大的草台班子,代码都是屎山堆出来的!>>
*
* 所谓时间切片,其实就是: 拉一会儿 → 出来透口气 → 再进去拉一会儿
*
* 实现方法巨简单:规定每个人最多在厕所里待多久
*
* 为什么是5ms?
* 这是无数程序员用血泪总结出来的经验值:
*
* (1)太短:刚脱裤子就要出来,效率太低(像便秘一样难受)
* (2)太长:后面排队的人要骂娘了(用户:这破网页卡成狗了!)
*
* 设计目标:在保证流畅性的前提下,最大化任务执行效率
*/
frameInterval: 5 // 时间切片的时间间隔(单位:毫秒)
};
(5)让出主线程
问题 :怎么知道一个人是不是占着茅坑不拉...?
回答:设置厕所使用时间限制!
javascript
// 5.ShouldYieldToHost.js
// 对应官方npm包:scheduler.production.js 中的 shouldYieldToHost
import { unstable_now as getCurrentTime } from './03.TimeTools.js';
import { SCHEDULER_STATE } from './04.SchedulerState.js';
// (1)设置下一个人的厕所使用截止时间 -- 你到 9:50就得出来
export function setDeadline() {
SCHEDULER_STATE.deadline = getCurrentTime() + SCHEDULER_STATE.frameInterval;
}
// (2)让出主线程
export function shouldYieldToHost() {
/**
* 什么情况下需要让出主线程?
* (1)截止时间到了
* (2)需要重绘
*/
return getCurrentTime() >= SCHEDULER_STATE.deadline || SCHEDULER_STATE.needsPaint; // 简写
}
(6)延时队列调度
问题 :有人预约10分钟后上厕所,怎么知道10分钟到了?
回答:设个闹钟,等会响了就来看!
javascript
// 6.AdvanceTimers.js
/**
* 任务推进 --对应官方npm包:scheduler.production.js 中的 advanceTimers 和 handleTimeout
* 官方源码的精髓:
* - 用最小堆管理延迟任务(timerQueue)
* - 定期把到期的延迟任务移到执行队列(taskQueue) advanceTimers
* - 智能设置下一次检查时间,避免不必要的检查 handleTimeout
*/
import { unstable_now as getCurrentTime } from './03.TimeTools.js';
import { peek, pop, push } from './02.MinHeap.js';
import { SCHEDULER_STATE } from './04.SchedulerState.js';
import { schedulePerformWorkUntilDeadline } from './08.PerformWorkUntilDeadline.js';
// (1)任务推进
// 作用:检查预约队列,有哪些人到时间了,到时间了就从预约队列移到等待队列
export function advanceTimers(currentTime) {
// 瞄一眼延时队列中的 堆顶元素(最早到期的任务)
let timerTask = peek(SCHEDULER_STATE.timerQueue);
while (timerTask !== null) {
// 有人取消了 - 这个人说 "我 *了,憋死算了,不上了🤬"
if (timerTask.callback === null) pop(SCHEDULER_STATE.timerQueue);
else if (timerTask.startTime <= currentTime) {
// 快要到时间了!从预约队列移到等待队列
pop(SCHEDULER_STATE.timerQueue);
timerTask.sortIndex = timerTask.expirationTime;
push(SCHEDULER_STATE.taskQueue, timerTask);
}
else return; // 堆顶元素都还没到时间,后面的也不用看了(因为最小堆是按时间排序的)
timerTask = peek(SCHEDULER_STATE.timerQueue); // 继续看下一个堆顶元素
}
}
// (2) 设置延迟检查定时器 - 对应官方:requestHostTimeout
// 作用:安排一个闹钟,在指定时间后再检查
export function requestHostTimeout(callback, delay) {
cancelHostTimeout(); // 不管三七二十一,一上来就先把上一个定时器清除了,管你有没有
taskTimeoutID = setTimeout(() => callback(getCurrentTime()), delay); // 将当前时间传递给回调函数;
}
// (3)取消超时定时器 - 对应官方:cancelHostTimeout
// 作用:清理之前设置的超时定时器,防止多个定时器冲突
export function cancelHostTimeout() {
if (SCHEDULER_STATE.taskTimeoutID !== -1) {
clearTimeout(SCHEDULER_STATE.taskTimeoutID);
SCHEDULER_STATE.taskTimeoutID = -1;
}
}
// (4) 调度延时任务并预定下一次检查得时间 - 对应官方:handleTimeout
// 作用:1.延时任务到期了就移到任务队列 2.有任务了就启动工作循环 3.没有任务了就设置下一次检查时间
/**
* 个人感觉这个函数名字取得不是,因为你从名字上不太能知道这个函数是干啥的,追问了ds
* 它说可以换一个名字 -- checkAndScheduleNext(检查并安排下一次)
*
* 【可以想象一个场景】:
* 新病人预约 → 护士记录到预约本(timerQueue)
* ↓
* 护士设置闹钟:2小时后检查(requestHostTimeout)
* ↓
* 【2小时后】闹钟响了!
* ↓
* 护士开始工作(checkAndScheduleNext):
1. 检查预约本,把到期的移到候诊区(advanceTimers)
2. 发现候诊区有病人 → 叫医生看病(启动工作循环)
3. 发现候诊区没病人 → 设置4小时后的闹钟
*/
export function handleTimeout(currentTime) {
// 这里的currentTime就是在requestHostTimeout中传递的当前时间
// 1. 当前延迟检查已完成,清除标志
SCHEDULER_STATE.isHostTimeoutScheduled = false;
// 2. 推进定时器,检查是否有延迟任务到期
advanceTimers(currentTime);
// 3. 如果还没有安排工作循环
if (!SCHEDULER_STATE.isHostCallbackScheduled) {
// (1)如果任务队列有任务:安排工作循环
if (peek(SCHEDULER_STATE.taskQueue) !== null) {
SCHEDULER_STATE.isHostCallbackScheduled = true; // 已经安排了工作循环来了哈!
schedulePerformWorkUntilDeadline() // 启动工作循环
// 如果是按照代码文件顺序来看的,看到这里可能会懵,没关系,先不管这个工作循环具体做了啥,因为代码实在后面才实现,等后面再回头来看这里就行
}
// (2)没有任务了,那就设置下一次检查时间
else {
const firstTimerTask = peek(SCHEDULER_STATE.timerQueue);
if (firstTimerTask !== null) {
const nextDelayTime = firstTimerTask.startTime - currentTime;
requestHostTimeout(handleTimeout, nextDelayTime); // 定个闹钟,等会再来检查
}
}
}
}
(7)核心工作循环
javascript
// 7.WorkLoop.js
/**
* 调度器核心工作循环实现 调度器的任务执行引擎,对应官方npm包中的 flushWork 和 workLoop 函数
*
* 核心功能:
* 1. flushWork - 工作循环的入口包装器,负责状态管理和异常安全
* 2. workLoop - 实际的任务执行循环,实现优先级调度和时间切片
*/
import { peek, pop } from './02.MinHeap.js';
import { unstable_now as getCurrentTime } from './03.TimeTools.js';
import { SCHEDULER_STATE } from './04.SchedulerState.js';
import { shouldYieldToHost } from './05.ShouldYieldToHost.js';
import { advanceTimers, handleTimeout, requestHostTimeout, cancelHostTimeout } from './06.AdvanceTimers.js';
// (1)调度器工作循环入口函数
export function flushWork(initialTime) {
// 重置调度状态,防止重复调度
SCHEDULER_STATE.isHostCallbackScheduled = false;
// 如果已经安排了延迟任务检查,就取消它
if (SCHEDULER_STATE.isHostTimeoutScheduled) {
SCHEDULER_STATE.isHostTimeoutScheduled = false;
cancelHostTimeout();
}
// 标记正在执行工作循环中....
SCHEDULER_STATE.isPerformingWork = true;
// 保存当前优先级
const previousPriorityLevel = SCHEDULER_STATE.currentPriorityLevel;
try {
// 工作循环开始!
return workLoop(initialTime);
}
finally {
// 清理当前任务引用 - 当前任务已完成或暂停
SCHEDULER_STATE.currentTask = null;
// 恢复原始优先级
SCHEDULER_STATE.currentPriorityLevel = previousPriorityLevel;
// 标记 工作循环已完成
SCHEDULER_STATE.isPerformingWork = false;
}
}
// (2)工作循环 --- 任务执行的核心逻辑
function workLoop(initialTime) {
let currentTime = initialTime;
// (1)如果定时器队列有任务要到时间了,那就抓到任务队列中来
advanceTimers(currentTime);
// (2)瞄一眼任务队列的第一个任务
const currentTask = peek(SCHEDULER_STATE.taskQueue);
// 开始叫号循环!
while (currentTask !== null) {
// 如果你还能憋并且必须归还浏览器执行权了,你就再撑一会,直接跳出循环,主线程要去干其他事情
if (currentTask.expirationTime > currentTime && shouldYieldToHost()) break;
const currentCallback = currentTask.callback;
if (typeof currentCallback === 'function') {
currentTask.callback = null;
SCHEDULER_STATE.currentPriorityLevel = currentTask.priorityLevel;
// 执行任务,看这个人是不是"便秘"(执行完是不是又返回了新的回调函数)
const didTimeout = currentTask.expirationTime <= currentTime;
const continuationCallback = currentCallback(didTimeout);
currentTime = getCurrentTime();
// 如果返回了新的函数 -- 这个人"便秘"了!还没拉完,下次继续,并且还是你这个人
if (typeof continuationCallback === 'function') currentTask.callback = continuationCallback;
else {
/**
* 为什么要检查 currentTask === peek(taskQueue)?
*
* 想象一下这个场景:
* 当前任务执行中:张三正在上厕所(currentTask 是张三)
* 执行过程中:李四突然插队(优先级更高的任务被加入队列)
* 任务完成时:张三上完厕所了,但此时队列的第一个已经不是张三了
*
* 如果你不检查,你以为没有新的任务进来直接把第一个给删除了,你猜李四会对你说什么😃
* (李四:我**尔冯了个福,你睁大你的🐕眼看清楚,老子不是张三,你删错人了)
*/
if (currentTask === peek(SCHEDULER_STATE.taskQueue)) {
pop(SCHEDULER_STATE.taskQueue);
}
}
advanceTimers(currentTime); // 执行完一个任务,就去检查预约队列
}
/**
* 💡 当前任务的callback不是函数?
* 想象:你等了半天想拉💩,结果发现只是个屁💨
* 这种浪费资源的,直接删除!
*/
else {
pop(SCHEDULER_STATE.taskQueue);
}
// 叫下一个
currentTask = peek(SCHEDULER_STATE.taskQueue);
}
/**
* 这里你会不会有疑问:while循环下去的条件就是currentTask不为空,为啥跳出循环了,还要判断一下呢?
*
* 这是因为,如果任务在执行过程中,shouldYieldToHost()为true,说明 是应该交出浏览器的执行权了这才导致的循环结束
*
* 于是,需要告诉调用者此次WorkLoop执行完后,是否还有任务
*/
if (currentTask !== null) return true;
else {
const firstTimerTask = peek(SCHEDULER_STATE.timerQueue);
if (firstTimerTask !== null) {
// 没人了,定个闹钟,下次再来检查
const timeoutTime = firstTimerTask.startTime - currentTime;
requestHostTimeout(handleTimeout, timeoutTime);
}
return false;
}
}
(8)暂停和恢复
问题 :怎么实现真正的"暂停"和"继续"?
答案:把未完成的任务包装成宏任务延后执行!
javascript
// 08.PerformWorkUntilDeadline.js
import { unstable_now as getCurrentTime } from './03.TimeTools.js';
import { flushWork } from './07.WorkLoop.js';
import { setDeadline } from './05.ShouldYieldToHost.js';
import { SCHEDULER_STATE } from './04.SchedulerState.js';
// (1)单次轮班工作
export function performWorkUntilDeadline() {
if (SCHEDULER_STATE.isPerformingWork) return; // 防止重复轮班
// 设置这次轮班的截止时间
setDeadline();
SCHEDULER_STATE.isPerformingWork = true;
const startTime = getCurrentTime();
let hasMoreWork = true;
try {
// 执行工作循环
hasMoreWork = flushWork(startTime);
} finally {
// 如果还有工作,安排下一轮
if (hasMoreWork) {
schedulePerformWorkUntilDeadline();
} else {
SCHEDULER_STATE.isHostCallbackScheduled = false;
}
SCHEDULER_STATE.isPerformingWork = false;
}
}
// (2)安排下一轮工作
export function schedulePerformWorkUntilDeadline() {
if (SCHEDULER_STATE.isHostCallbackScheduled) return;
SCHEDULER_STATE.isHostCallbackScheduled = true;
/**
* 🎯 为什么用MessageChannel不用setTimeout?
* - setTimeout有最小延迟(通常4ms)
* - setTimeout会被浏览器节流
* - MessageChannel真正零延迟!
*/
if (typeof MessageChannel !== 'undefined') {
const channel = new MessageChannel();
channel.port1.onmessage = performWorkUntilDeadline;
channel.port2.postMessage(null); // 触发异步消息
} else {
// 老浏览器用setTimeout凑合
setTimeout(performWorkUntilDeadline, 0);
}
}
恍然大悟时刻:噢!原来"暂停"就是把未完成的任务包装成宏任务延后执行!
(9)调度callbackFn
想象 :有人来到厕所管理处:"我要上厕所!"
管理员:"好的,告诉我你有多急?想什么时候上?我把你安排到不同队列中排队"
javascript
// 09.ScheduleCallback.js
import { unstable_now as getCurrentTime, getTimeoutByPriority } from './03.TimeTools.js';
import { push, peek } from './02.MinHeap.js';
import { SCHEDULER_STATE } from './04.SchedulerState.js';
import { schedulePerformWorkUntilDeadline } from './08.PerformWorkUntilDeadline.js';
import { requestHostTimeout, cancelHostTimeout } from './06.AdvanceTimers.js';
// 核心调度API
export function unstable_scheduleCallback(priorityLevel, callback, options) {
const currentTime = getCurrentTime();
// 计算开始时间
let startTime = currentTime;
if (options && typeof options.delay === 'number' && options.delay > 0) {
startTime = currentTime + options.delay; // 预约上厕所
}
// 计算能憋多久
const timeout = getTimeoutByPriority(priorityLevel);
const expirationTime = startTime + timeout;
// 创建人员档案
const newTask = {
id: SCHEDULER_STATE.taskIdCounter++,
callback,
priorityLevel,
startTime,
expirationTime,
sortIndex: -1, // 临时值
};
// 分配到不同队列
if (startTime > currentTime) handleDelayedTask(newTask, startTime, currentTime); // 预约队列
else handleImmediateTask(newTask, expirationTime); // 等待队列
return newTask;
}
// (1)处理立即执行的人
function handleImmediateTask(newTask, expirationTime) {
newTask.sortIndex = expirationTime;
push(SCHEDULER_STATE.taskQueue, newTask);
// 如果没安排叫号员,就安排一个
if (!SCHEDULER_STATE.isHostCallbackScheduled && !SCHEDULER_STATE.isPerformingWork) {
SCHEDULER_STATE.isHostCallbackScheduled = true;
schedulePerformWorkUntilDeadline();
}
}
// (2)处理预约的人
function handleDelayedTask(newTask, startTime, currentTime) {
newTask.sortIndex = startTime;
push(SCHEDULER_STATE.timerQueue, newTask);
// 如果是第一个预约的,要设置闹钟
if (peek(SCHEDULER_STATE.taskQueue) === null && newTask === peek(SCHEDULER_STATE.timerQueue)) {
// 取消旧闹钟,设置新闹钟
if (SCHEDULER_STATE.isHostTimeoutScheduled) cancelHostTimeout();
else SCHEDULER_STATE.isHostTimeoutScheduled = true;
const delay = startTime - currentTime;
requestHostTimeout(handleTimeout, delay);
}
}
至此,一个调度器的核心设计已经完成,下面是一些相关且必要的api函数
(10)取消 & 查询
javascript
// 10.CancelAndQuery.js
import { SCHEDULER_STATE } from './04.SchedulerState.js';
// (1)取消任务
export function unstable_cancelCallback(task) {
/**
* 想象:有人预约了但突然不想上了
* "管理员,把我的预约取消吧!"
*
* 设计精髓:不立即删除,只是标记,避免破坏堆结构
* 叫号时会自动跳过标记的人
*/
task.callback = null;
}
// (2)查询当前优先级
export function unstable_getCurrentPriorityLevel() return SCHEDULER_STATE.currentPriorityLevel;
(11)优先级变更
javascript
// 11.PriorityControl.js
import { SCHEDULER_STATE } from './04.SchedulerState.js';
// (1)临时提高身份
export function unstable_runWithPriority(priorityLevel, eventHandler) {
/**
* 🎭 临时VIP卡
* 想象:普通顾客说"我有急事!让我当一回VIP!"
*/
const previousPriorityLevel = SCHEDULER_STATE.currentPriorityLevel;
SCHEDULER_STATE.currentPriorityLevel = priorityLevel;
try {
return eventHandler();
} finally {
// 用完就恢复,不能一直占着VIP 认清自我定位!
SCHEDULER_STATE.currentPriorityLevel = previousPriorityLevel;
}
}
// (2)临时降级
export function unstable_next(eventHandler) {
/**
* 想象:领导进厕所只是为了洗个手
* "这事不急,就按普通员工处理"
*/
let priorityLevel;
// 智能降级规则
switch (SCHEDULER_STATE.currentPriorityLevel) {
case 1: // 董事长
case 2: // 急尿员工
case 3: // 普通员工
priorityLevel = 3; // 都降级到普通
break;
default:
priorityLevel = SCHEDULER_STATE.currentPriorityLevel;
}
const previousPriorityLevel = SCHEDULER_STATE.currentPriorityLevel;
SCHEDULER_STATE.currentPriorityLevel = priorityLevel;
try {
return eventHandler();
} finally {
// 恢复领导身份
SCHEDULER_STATE.currentPriorityLevel = previousPriorityLevel;
}
}
(12)绘制&帧率
javascript
// 12.PaintUtils.js
/**
* 1. 请求绘制
* 2. 帧率控制
*/
import { SCHEDULER_STATE } from './04.SchedulerState.js'
// (1)请求重新绘制 - 对应官方 unstable_requestPaint
export function unstable_requestPaint() {
SCHEDULER_STATE.needsPaint = true;
}
// (2)动态调整时间切片长度 - 对应官方 unstable_forceFrameRate
// 官方限制:0-125fps(太快了也受不了🫨)
export function unstable_forceFrameRate(fps) {
if (fps < 0 || fps > 125) {
// Using console['error'] to evade Babel and ESLint
console['error'](
'forceFrameRate takes a positive int between 0 and 125, ' +
'forcing frame rates higher than 125 fps is not supported',
);
return;
}
if (fps > 0) SCHEDULER_STATE.frameInterval = Math.floor(1000 / fps);
else SCHEDULER_STATE.frameInterval = 5; // 默认 5ms
}
(13)优先级马甲
问题 :在异步回调中,如何保持创建时的优先级?
想象:给回调函数穿上"优先级马甲",不管在哪调用都保持身份!
javascript
// 13.WrapCallback.js
import { SCHEDULER_STATE } from './04.SchedulerState.js';
export function unstable_wrapCallback(callback) {
// 记下【创建时】的优先级(比如是VIP:2)
const parentPriorityLevel = SCHEDULER_STATE.currentPriorityLevel;
// 返回穿马甲的函数 给外部环境调用
return function() {
// 保存 【调用时】的优先级(可能是普通:3)
const previousPriorityLevel = SCHEDULER_STATE.currentPriorityLevel;
SCHEDULER_STATE.currentPriorityLevel = parentPriorityLevel; // 切换到创建时的VIP身份
try {
return callback.apply(this, arguments);
} finally {
// 恢复调用时的身份 不影响其他任务调度
SCHEDULER_STATE.currentPriorityLevel = previousPriorityLevel;
}
};
}
tips:这个函数具体的作用,以及没有这个函数会有啥影响,详见文末附上的仓库地址
(14)完结
javascript
// 14.Scheduler.js
/**
* 导出所有API供外部使用
*/
import {
ImmediatePriority,
UserBlockingPriority,
NormalPriority,
LowPriority,
IdlePriority
} from './01.Priorities.js';
import { unstable_now } from './03.TimeTools.js';
import { shouldYieldToHost } from './05.ShouldYieldToHost.js';
import { unstable_scheduleCallback } from './09.ScheduleCallback.js';
import { unstable_cancelCallback, unstable_getCurrentPriorityLevel } from './10.CancelAndQuery.js';
import { unstable_runWithPriority, unstable_next } from './11.PriorityControl.js';
import { unstable_requestPaint, unstable_forceFrameRate } from './12.PaintUtils.js';
import { unstable_wrapCallback } from './13.WrapCallback.js';
// 命名导出 -- 依旧对齐官方
export {
ImmediatePriority as unstable_ImmediatePriority,
UserBlockingPriority as unstable_UserBlockingPriority,
NormalPriority as unstable_NormalPriority,
LowPriority as unstable_LowPriority,
IdlePriority as unstable_IdlePriority,
unstable_scheduleCallback,
unstable_cancelCallback,
unstable_getCurrentPriorityLevel,
unstable_runWithPriority,
unstable_next,
unstable_requestPaint,
unstable_forceFrameRate,
unstable_wrapCallback,
shouldYieldToHost as unstable_shouldYield,
unstable_now
};
本文跟官方源码有差异,但已完整包含了React的Scheduler调度器的核心
完整代码详见:https://github.com/ZenithAurora/Scheduler.git 此致
三、尾声
当我们回溯这段从零构建调度器的旅程,会发现它早已超越了代码的疆域,成为了一面映照生命本质的明镜。React调度器的精魂,不在其精妙的算法,而在其深藏的 让渡 哲学------懂得何时坚持,更学会及时放手。
每一个任务都渴望执行到底,如我们生命中每一个执念都渴望圆满。然而调度器告诉我们:真正的智慧不是一味地抢占,而是在恰当的时机主动让出主线程。这何尝不是一种人生的启示?在奔涌的生命长河中,我们都需要学会在专注与放手之间找到平衡------全力以赴地投入,却也懂得在力竭前暂歇;珍视每一个当下的时刻,却也给未来留出呼吸的空间。
精心设计的五级优先级,恰如我们生命中事务的轻重缓急。有些如"董事长内急",必须立即响应;有些如"摸鱼员工",可以从容安排。生命的艺术,就在于不为琐碎耗尽心神,而为重要保留能量。
最小堆的排序智慧提醒我们:效率源自秩序。在杂乱无章中,我们耗费心力;在清晰条理中,我们举重若轻。
而最动人的,莫过于调度器中蕴藏的慈悲------它不让任何任务永远等待,也不让任何任务无限独占。这是对有限资源的温柔使用,也是对每个需求的平等尊重。
React团队的工程师们用五年时间打磨这个调度器,他们解决的不仅仅是技术性能问题,更是在数字世界中建立了一套优雅的秩序。这种对完美的追求正诠释着:
------耐心对待所有尚未解决的事情,试着去爱问题本身