【react18原理探究实践】调度器(Scheduler)原理深度解析

🧑‍💻 前言

在上一篇文章里,我们已经走到 scheduleUpdateOnFiber,它会把更新注册到 Scheduler 调度器

那么 React 是如何利用 Scheduler 来决定「哪个任务先执行、哪个可以延迟」,以及如何实现「时间切片 + 可中断渲染」的呢?

本篇我们就来源码级别地拆解 Scheduler 的实现。

上图来自图解react


1. 调度器整体架构

React Scheduler 大体分为三个部分:

js 复制代码
Scheduler 架构
│
├─ 调度内核 (SchedulerHostConfig.js)
│  ├─ requestHostCallback     - 请求宿主环境执行回调
│  ├─ cancelHostCallback      - 取消回调
│  ├─ shouldYieldToHost       - 是否应该让出主线程
│  └─ getCurrentTime          - 获取当前时间
│
├─ 任务队列管理 (Scheduler.js)
│  ├─ taskQueue   - 立即任务队列(最小堆)
│  ├─ timerQueue  - 延迟任务队列(最小堆)
│  ├─ unstable_scheduleCallback - 任务调度入口
│  └─ workLoop    - 消费任务的循环
│
└─ 优先级系统
   ├─ ImmediatePriority      - 立即执行 (-1ms)
   ├─ UserBlockingPriority   - 用户阻塞 (250ms)
   ├─ NormalPriority         - 正常 (5000ms)
   ├─ LowPriority            - 低优先级 (10000ms)
   └─ IdlePriority           - 空闲 (最大值,不会过期)

一句话总结:Scheduler = React 的任务大脑,负责优先级、队列和调度循环。


2. 调度内核实现

2.1 消息循环机制

调度器会通过 performWorkUntilDeadline 来消费任务,利用不同环境的「异步 API」来驱动循环。

js 复制代码
// 核心调度函数
const performWorkUntilDeadline = () => {
  if (scheduledHostCallback !== null) {
    const currentTime = getCurrentTime();
    // 设置时间切片截止时间
    deadline = currentTime + yieldInterval; // 默认 5ms
    const hasTimeRemaining = true;
    
    try {
      // 执行调度回调(flushWork),返回是否还有剩余工作
      const hasMoreWork = scheduledHostCallback(hasTimeRemaining, currentTime);
      if (!hasMoreWork) {
        // 没有剩余任务,关闭消息循环
        isMessageLoopRunning = false;
        scheduledHostCallback = null;
      } else {
        // 还有任务,继续调度下一次
        port.postMessage(null);
      }
    } catch (error) {
      // 出错也要继续调度,避免中断
      port.postMessage(null);
      throw error;
    }
  } else {
    isMessageLoopRunning = false;
  }
  needsPaint = false;
};

解释:

  • scheduledHostCallback → 保存当前要执行的工作函数(一般是 flushWork)。
  • deadline → 本次时间切片的结束时间,默认为 5ms。
  • 如果任务执行完了 → 退出循环;否则继续通过 MessageChannel 或其他方式调度下一次。

2.2 调度机制选择

Scheduler 会在不同环境下选择最优的消息调度方式:

js 复制代码
let schedulePerformWorkUntilDeadline;

if (typeof localSetImmediate === 'function') {
  // Node.js 环境,优先使用 setImmediate
  schedulePerformWorkUntilDeadline = () => {
    localSetImmediate(performWorkUntilDeadline);
  };
} else if (typeof MessageChannel !== 'undefined') {
  // 浏览器环境,避免 setTimeout 的 4ms 延迟
  const channel = new MessageChannel();
  const port = channel.port2;
  channel.port1.onmessage = performWorkUntilDeadline;
  schedulePerformWorkUntilDeadline = () => {
    port.postMessage(null);
  };
} else {
  // 兜底:setTimeout
  schedulePerformWorkUntilDeadline = () => {
    localSetTimeout(performWorkUntilDeadline, 0);
  };
}

解释:

  • Node.jssetImmediate(最快)
  • 浏览器MessageChannel(避免 4ms 延迟)
  • 兜底setTimeout

这保证了 Scheduler 在不同运行环境下都能以「最小延迟」触发任务。


2.3 时间切片实现

每次调度循环都会判断时间片,判断是否过期,是否超过时间片,是否有高优先级任务,判断是否跳出本次循环

js 复制代码
let yieldInterval = 5; // 默认每个切片 5ms
let deadline = 0;
const maxYieldInterval = 300;
let needsPaint = false;

// 判断是否需要让出主线程
shouldYieldToHost = function () {
  const currentTime = getCurrentTime();
  if (currentTime >= deadline) {
    // 超过了本次时间切片的截止点
    if (needsPaint || (navigator.scheduling && navigator.scheduling.isInputPending())) {
      // 有待绘制或有用户输入 → 立即让出
      return true;
    }
    // 即使没有用户操作,也要避免超过最大间隔
    return currentTime >= maxYieldInterval;
  } else {
    // 还没到 5ms 截止点,可以继续执行
    return false;
  }
};

解释:

  • 每次任务执行时都会检查是否超过 时间片截止点 (5ms)
  • 如果超时 → 让出主线程给浏览器绘制或用户交互。
  • 最长也不会超过 300ms,避免长时间霸占线程。

3. 任务创建和管理

3.1 任务创建流程

unstable_scheduleCallback 是外部调用的入口,用来创建一个任务。

js 复制代码
function unstable_scheduleCallback(priorityLevel, callback, options) {
  var currentTime = getCurrentTime();

  // 计算任务开始时间
  var startTime;
  if (typeof options === 'object' && options !== null) {
    var delay = options.delay;
    startTime = (typeof delay === 'number' && delay > 0)
      ? currentTime + delay
      : currentTime;
  } else {
    startTime = currentTime;
  }

  // 根据优先级确定过期时间
  var timeout;
  switch (priorityLevel) {
    case ImmediatePriority: timeout = -1; break;
    case UserBlockingPriority: timeout = 250; break;
    case IdlePriority: timeout = IDLE_PRIORITY_TIMEOUT; break;
    case LowPriority: timeout = 10000; break;
    case NormalPriority:
    default: timeout = 5000; break;
  }

  var expirationTime = startTime + timeout;

  // 构建任务对象
  var newTask = {
    id: taskIdCounter++,
    callback,
    priorityLevel,
    startTime,
    expirationTime,
    sortIndex: -1,
  };

  // 判断放入哪个队列
  if (startTime > currentTime) {
    // 延迟任务 → timerQueue
    newTask.sortIndex = startTime;
    push(timerQueue, newTask);
    if (peek(taskQueue) === null && newTask === peek(timerQueue)) {
      requestHostTimeout(handleTimeout, startTime - currentTime);
    }
  } else {
    // 立即任务 → taskQueue
    newTask.sortIndex = expirationTime;
    push(taskQueue, newTask);
    if (!isHostCallbackScheduled && !isPerformingWork) {
      isHostCallbackScheduled = true;
      requestHostCallback(flushWork);
    }
  }

  return newTask;
}

解释:

  1. 计算 startTime → 是否有 delay 参数。

  2. 计算过期时间 expirationTime → 根据优先级超时策略。

  3. 构建任务对象 → 包含 id、callback、优先级、开始/过期时间。

  4. 入队

    • 延迟任务 → timerQueue
    • 立即任务 → taskQueue 并触发调度 (requestHostCallback)

3.2 任务对象结构

js 复制代码
var newTask = {
  id: taskIdCounter++,           // 唯一任务 ID
  callback,                      // 任务执行函数
  priorityLevel,                 // 优先级
  startTime,                     // 任务开始时间
  expirationTime,                // 过期时间
  sortIndex: expirationTime,     // 堆排序用的索引
};

3.3 不同优先级的超时时间

js 复制代码
var IMMEDIATE_PRIORITY_TIMEOUT = -1;          // 立即执行
var USER_BLOCKING_PRIORITY_TIMEOUT = 250;     // 250ms
var NORMAL_PRIORITY_TIMEOUT = 5000;           // 5s
var LOW_PRIORITY_TIMEOUT = 10000;             // 10s
var IDLE_PRIORITY_TIMEOUT = maxSigned31BitInt;// 无限期

解释:

  • 高优先级(立即) → 马上过期 → 立刻执行
  • 普通优先级 → 最长可等 5s
  • Idle → 理论上永远不过期,等主线程空闲时再执行

3.4 任务的callback是啥

我们刚才说到:unstable_scheduleCallback 会生成一个 task ,并把它的 callback 存在 task 对象里。那这个 callback 到底一般是什么呢?

task 的 callback 来源

在 React 内部,Scheduler 被 React Fiber 调用,常见的 callback 有:

  1. Fiber 渲染任务

    • 比如 React 发起一次渲染更新 (scheduleUpdateOnFiber) 时,最终会调度一个任务:

      js 复制代码
      scheduleCallback(
        NormalPriority, // 或更高优先级
        performConcurrentWorkOnRoot.bind(null, root) 
      );
    • 这里的 callback 就是 performConcurrentWorkOnRoot,它负责执行 Fiber 的并发渲染流程。

  2. 同步任务

    • 当需要同步更新(比如事件处理、flushSync)时,Scheduler 会调度:

      js 复制代码
      scheduleSyncCallback(performSyncWorkOnRoot.bind(null, root));
    • 这里的 callback 就是 performSyncWorkOnRoot,它会直接在本次任务里完成整个 Fiber 渲染,不做时间切片。

  3. 继续执行的续接函数 (continuation)

    • 如果一次时间片用完了,但 Fiber 树还没构建完,performConcurrentWorkOnRoot 会返回一个新的函数(就是 continuation):

      js 复制代码
      function performConcurrentWorkOnRoot(root, didTimeout) {
        // ... 构建 Fiber 树
        if (workInProgress !== null) {
          // 没做完,返回续接函数
          return performConcurrentWorkOnRoot.bind(null, root);
        }
        // 做完了返回 null
        return null;
      }
    • 这样 Scheduler 在下一个时间片会继续执行这个返回的函数,相当于把「未完成的任务」保存了下来,这就是可中断的关键,fiber保存了执行环境,返回的performConcurrentWorkOnRoot可以在后续重新执行。

  4. 其他 React 内部任务

    • 比如 被动副作用 (Passive Effects) 的 flush 任务。

    • React 会调用:

      js 复制代码
      scheduleCallback(NormalPriority, flushPassiveEffects);
    • 此时 callback 就是 flushPassiveEffects

所以总结一下:

  • Fiber 渲染主任务performConcurrentWorkOnRoot(异步,可中断)
  • 同步渲染任务performSyncWorkOnRoot(一次完成)
  • 续接任务 → 上一次未完成的渲染函数继续执行
  • 副作用任务flushPassiveEffects

4. 任务调度执行

4.1 workLoop

workLoop 是任务真正的消费循环:

js 复制代码
function workLoop(hasTimeRemaining, initialTime) {
  let currentTime = initialTime;
  advanceTimers(currentTime); // 把到期的延迟任务转移到 taskQueue
  currentTask = peek(taskQueue); // 取队头任务
  
  while (currentTask !== null) {
    if (
      currentTask.expirationTime > currentTime &&
      (!hasTimeRemaining || shouldYieldToHost())
    ) {
      // 任务没过期,但时间片已用完 → 中断
      break;
    }
    
    const callback = currentTask.callback;
    if (typeof callback === 'function') {
      currentTask.callback = null;
      const didUserCallbackTimeout = currentTask.expirationTime <= currentTime;
      
      // 执行任务回调
      const continuationCallback = callback(didUserCallbackTimeout);
      currentTime = getCurrentTime();
      
      if (typeof continuationCallback === 'function') {
        // 返回了续接函数 → 任务没完成,下次继续
        currentTask.callback = continuationCallback;
      } else {
        // 任务完成 → 出队
        if (currentTask === peek(taskQueue)) {
          pop(taskQueue);
        }
      }
      advanceTimers(currentTime);
    } else {
      // 被取消的任务,直接出队
      pop(taskQueue);
    }
    currentTask = peek(taskQueue);
  }
  
  // 返回是否还有剩余任务
  if (currentTask !== null) {
    return true;
  } else {
    const firstTimer = peek(timerQueue);
    if (firstTimer !== null) {
      requestHostTimeout(handleTimeout, firstTimer.startTime - currentTime);
    }
    return false;
  }
}

解释:

  • 取出队列头任务(优先级最高)。
  • 检查是否要 让出线程(时间片到点/未过期)。
  • 执行任务回调,如果回调返回 续接函数,说明任务未完成,挂回队列。
  • 如果回调为空(被取消),则跳过。
  • 最后返回是否还有剩余任务。

4.2 时间切片退出条件

workLoop 退出有三种情况:

  1. 队列清空 → currentTask === null
  2. 超过时间片 → shouldYieldToHost() === true
  3. 任务未过期但时间片已用完 → 暂停,下次再执行

5. 可中断渲染

React 渲染过程中,Fiber 的构建也会被包装成 Scheduler 的任务,因此具备可中断性。

js 复制代码
function performConcurrentWorkOnRoot(root, didTimeout) {
  // ... 渲染 Fiber 树逻辑

  if (workInProgress !== null) {
    // Fiber 树还没构建完,返回续接函数
    return performConcurrentWorkOnRoot.bind(null, root);
  } else {
    // 构建完成
    return null;
  }
}

解释:

  • 如果任务没做完,返回一个新的函数 → 下次调度继续执行。
  • 如果做完了,返回 null

这就是 React 实现「可中断渲染」的关键。


6. 任务取消机制

6.1 取消实现

js 复制代码
function unstable_cancelCallback(task) {
  // 标记任务为取消状态
  task.callback = null;
}

6.2 原理

  • 任务队列是 最小堆,不能直接删除中间节点。
  • 所以取消就是 置空 callback
  • workLoop 里会检查,如果 callback 为空就直接跳过。

7. 节流防抖优化

React 内部通过 ensureRootIsScheduled 对任务调度进行优化:

js 复制代码
function ensureRootIsScheduled(root, currentTime) {
  const existingCallbackNode = root.callbackNode;
  const nextLanes = getNextLanes(root, workInProgressRootRenderLanes);
  const newCallbackPriority = returnNextLanesPriority();
  
  if (nextLanes === NoLanes) {
    return; // 没有工作需要调度
  }
  
  // 优化逻辑:节流 + 防抖
  if (existingCallbackNode !== null) {
    const existingCallbackPriority = root.callbackPriority;
    if (existingCallbackPriority === newCallbackPriority) {
      // 优先级相同 → 复用现有调度(节流)
      return;
    }
    // 优先级不同 → 取消旧任务(防抖)
    cancelCallback(existingCallbackNode);
  }
  
  // 新建调度任务
  let newCallbackNode;
  if (newCallbackPriority === SyncLane) {
    newCallbackNode = scheduleSyncCallback(performSyncWorkOnRoot.bind(null, root));
  } else {
    const schedulerPriorityLevel = lanesToSchedulerPriority(nextLanes);
    newCallbackNode = scheduleCallback(
      schedulerPriorityLevel,
      performConcurrentWorkOnRoot.bind(null, root),
    );
  }
  
  root.callbackPriority = newCallbackPriority;
  root.callbackNode = newCallbackNode;
}

解释:

  • 节流:如果优先级相同,直接复用原任务,不重复调度。
  • 防抖:如果优先级更高,取消旧的,换新任务。

8. 调度流程时序图

js 复制代码
用户交互 / setState
│
├─ scheduleUpdateOnFiber
│   └─ ensureRootIsScheduled
│       └─ unstable_scheduleCallback
│           └─ push(taskQueue, task)
│           └─ requestHostCallback(flushWork)
│
└─ 宿主环境 (MessageChannel / setImmediate)
    └─ performWorkUntilDeadline
        └─ flushWork
            └─ workLoop
                ├─ 时间切片检查
                ├─ 执行 task.callback
                ├─ 检查续接回调
                └─ 返回是否有剩余任务

9. 核心特性总结

  • 时间切片 (Time Slicing)

    • 每片 5ms,避免长任务阻塞 UI。
    • 超时/有输入时会让出线程。
  • 可中断渲染 (Interruptible Rendering)

    • Fiber 渲染任务可以暂停 & 恢复。
    • 高优先级任务能打断低优先级任务。
  • 优先级调度

    • Immediate > UserBlocking > Normal > Low > Idle
    • 通过过期时间和最小堆保证正确顺序。
  • 性能优化

    • 节流防抖避免重复调度。
    • 任务取消机制减少无效开销。

✅ 总结

Scheduler 的本质就是:

任务优先级 + 时间切片 + 可中断执行

让 React 能在不阻塞主线程的情况下,流畅地调度渲染和更新。

相关推荐
路漫漫心远3 小时前
音视频学习笔记十八——图像处理之OpenCV检测
前端
摸着石头过河的石头3 小时前
从零开始玩转前端:一站式掌握Web开发基础知识
前端·javascript
sniper_fandc3 小时前
关于Mybatis-Plus的insertOrUpdate()方法使用时的问题与解决—数值精度转化问题
java·前端·数据库·mybatisplus·主键id
10岁的博客3 小时前
技术博客SEO优化全攻略
前端
南屿im4 小时前
别再被引用坑了!JavaScript 深浅拷贝全攻略
前端·javascript
想要一辆洒水车4 小时前
vuex4源码分析学习
前端
sophie旭4 小时前
一道面试题,开始性能优化之旅(6)-- 异步任务和性能
前端·javascript·性能优化
年少不知有林皇错把梅罗当球王4 小时前
vue2、vue3中使用pb(Base64编码)
前端
FanetheDivine4 小时前
常见的AI对话场景和特殊情况
前端·react.js