从零实现React Scheduler调度器

一、引言

(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团队的工程师们用五年时间打磨这个调度器,他们解决的不仅仅是技术性能问题,更是在数字世界中建立了一套优雅的秩序。这种对完美的追求正诠释着:

------耐心对待所有尚未解决的事情,试着去爱问题本身

相关推荐
心随雨下6 分钟前
typescript中Triple-Slash Directives如何使用
前端·javascript·typescript
自在极意功。11 分钟前
AJAX 深度详解:从基础原理到项目实战
前端·ajax·okhttp
s***45312 分钟前
SpringBoot返回文件让前端下载的几种方式
前端·spring boot·后端
海上彼尚17 分钟前
[逆向] 1.本地登录爆破
前端·安全
什么时候吃饭21 分钟前
vue2、vue3父子组件嵌套生命周期执行顺序
前端·vue.js
2501_9409439122 分钟前
体系课\ Python Web全栈工程师
开发语言·前端·python
q***064723 分钟前
SpringSecurity相关jar包的介绍
android·前端·后端
低保和光头哪个先来29 分钟前
场景2:Vue Router 中 query 与 params 的区别
前端·javascript·vue.js·前端框架
q***952242 分钟前
SpringMVC 请求参数接收
前端·javascript·算法
|晴 天|44 分钟前
Vite 为何能取代 Webpack?新一代构建工具的崛起
前端·webpack·node.js