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

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

相关推荐
徐同保2 小时前
使用yarn@4.6.0装包,项目是react+vite搭建的,项目无法启动,报错:
前端·react.js·前端框架
Qrun3 小时前
Windows11安装nvm管理node多版本
前端·vscode·react.js·ajax·npm·html5
中国lanwp3 小时前
全局 npm config 与多环境配置
前端·npm·node.js
JELEE.4 小时前
Django登录注册完整代码(图片、邮箱验证、加密)
前端·javascript·后端·python·django·bootstrap·jquery
TeleostNaCl6 小时前
解决 Chrome 无法访问网页但无痕模式下可以访问该网页 的问题
前端·网络·chrome·windows·经验分享
前端大卫7 小时前
为什么 React 中的 key 不能用索引?
前端
你的人类朋友7 小时前
【Node】手动归还主线程控制权:解决 Node.js 阻塞的一个思路
前端·后端·node.js
小李小李不讲道理9 小时前
「Ant Design 组件库探索」五:Tabs组件
前端·react.js·ant design
毕设十刻9 小时前
基于Vue的学分预警系统98k51(程序 + 源码 + 数据库 + 调试部署 + 开发环境配置),配套论文文档字数达万字以上,文末可获取,系统界面展示置于文末
前端·数据库·vue.js