React Scheduler 源码分析:低优先级任务的中断与恢复机制

83 阅读8分钟

1. 概述

React Scheduler 是 React 并发特性的核心调度系统,负责管理不同优先级的任务执行。它通过智能的任务调度和时间切片机制,实现了高性能的用户界面渲染,同时保持了良好的用户体验。

1.1 核心特性

  • 优先级调度:支持5级优先级系统
  • 时间切片:连续执行时间限制,避免长时间阻塞
  • 协作式中断:任务主动让出,支持高优先级任务抢占
  • 状态保持:中断后的任务从中断点继续执行

1.2 优先级级别

// 从高到低的优先级
ImmediatePriority      // 立即执行
UserBlockingPriority   // 用户阻塞优先级  
NormalPriority         // 正常优先级
LowPriority           // 低优先级
IdlePriority          // 空闲优先级

2. 核心架构

2.1 数据结构

Task 任务对象

export opaque type Task = {
  id: number,                    // 唯一标识符
  callback: Callback | null,     // 任务回调函数(可能是延续回调)
  priorityLevel: PriorityLevel,  // 优先级级别
  startTime: number,             // 开始时间
  expirationTime: number,        // 过期时间
  sortIndex: number,             // 排序索引
  isQueued?: boolean,            // 是否已入队
};

双队列设计

// 任务队列:存储立即执行的任务
var taskQueue: Array<Task> = [];     // 按 sortIndex 排序(最小堆)

// 定时器队列:存储延迟执行的任务  
var timerQueue: Array<Task> = [];    // 按 startTime 排序

2.2 核心变量

var currentTask = null;              // 当前执行的任务
var currentPriorityLevel = NormalPriority;  // 当前优先级级别
var isPerformingWork = false;        // 是否正在执行工作
var isHostCallbackScheduled = false; // 是否已安排主机回调
var isHostTimeoutScheduled = false;  // 是否已安排主机超时

3. 任务中断机制

3.1 中断触发的三个时机

A. 时间片耗尽中断

function shouldYieldToHost(): boolean {
  if (!enableAlwaysYieldScheduler && enableRequestPaint && needsPaint) {
    return true; // 需要绘制,立即让出
  }
  
  const timeElapsed = getCurrentTime() - startTime;
  if (timeElapsed < frameInterval) {
    return false; // 时间片未用完,继续执行
  }
  return true;    // 时间片用完,让出控制权
}

工作原理

  • 连续执行时间限制(来自 frameYieldMs 默认时间片长度配置值(5),表示连续执行的最大时间限制,可通过 forceFrameRate 调整)
  • 低优先级任务执行时间过长时,时间片耗尽
  • 强制让出控制权,允许高优先级任务执行

B. 高优先级任务插入中断

function advanceTimers(currentTime: number) {
  let timer = peek(timerQueue);
  while (timer !== null) {
    if (timer.startTime <= currentTime) {
      // 延迟的高优先级任务到期,转移到主队列
      pop(timerQueue);
      timer.sortIndex = timer.expirationTime;  // 设置排序索引为过期时间
      push(taskQueue, timer);                  // 插入主队列
    }
  }
}

工作原理

  • 高优先级任务可能被延迟执行(使用delay选项)
  • 延迟到期时,通过advanceTimers转移到主队列
  • 下次workLoop执行时,高优先级任务会优先执行
  • 注意:高优先级任务插入后不会立即中断当前执行的任务,而是在下次调度循环中优先执行

C. 任务主动让出中断

// 在workLoop中
const continuationCallback = callback(didUserCallbackTimeout);

if (typeof continuationCallback === 'function') {
  // 任务返回延续回调,表示需要继续执行
  currentTask.callback = continuationCallback;
  if (enableProfiling) {
    markTaskYield(currentTask, currentTime);
  }
  advanceTimers(currentTime);
  return true; // 立即让出控制权
}

工作原理

  • 长任务可以主动返回延续回调
  • 立即让出控制权,允许其他任务执行
  • 下次调度时继续执行剩余工作

3.2 中断的具体流程

function workLoop(initialTime: number) {
  let currentTime = initialTime;
  advanceTimers(currentTime);        // 推进定时器,可能将高优先级任务加入队列
  currentTask = peek(taskQueue);     // 获取当前最高优先级任务
  
  while (currentTask !== null) {
    // 检查是否需要让出控制权
    if (!enableAlwaysYieldScheduler) {
      if (currentTask.expirationTime > currentTime && shouldYieldToHost()) {
        break; // 退出工作循环,让出给主线程
      }
    }
    
    // 执行当前任务
    const callback = currentTask.callback;
    if (typeof callback === 'function') {
      const continuationCallback = callback(didUserCallbackTimeout);
      
      if (typeof continuationCallback === 'function') {
        // 任务需要继续执行,立即让出
        currentTask.callback = continuationCallback;
        return true;
      } else {
        // 任务完成,从队列中移除
        pop(taskQueue);
      }
    }
    
    // 获取下一个任务
    currentTask = peek(taskQueue);
  }
}

4. 任务恢复机制

4.1 核心机制:延续回调(Continuation Callback)

React Scheduler 通过延续回调机制实现任务从中断点继续执行,而不是重新执行:

// 任务执行过程中主动让出
const continuationCallback = callback(didUserCallbackTimeout);

if (typeof continuationCallback === 'function') {
  // 关键:保存延续回调到任务对象中
  currentTask.callback = continuationCallback;
  
  if (enableProfiling) {
    markTaskYield(currentTask, currentTime); // 标记任务让出
  }
  
  // 让出控制权,但任务状态已保存
  return true;
}

4.2 任务状态保持

状态保存过程

// 步骤1:任务主动让出(在调度器内部)
function longRunningTask() {
  // 执行部分工作...
  
  // 任务返回延续回调,表示需要继续执行
  return () => {
    // 继续执行剩余工作
    return continueRemainingWork();
  };
}

// 辅助函数:继续执行剩余工作(概念性示例)
function continueRemainingWork() {
  // 继续执行剩余的工作...
  return null; // 完成
}

// 步骤2:调度器保存状态
const continuationCallback = callback(didUserCallbackTimeout);

if (typeof continuationCallback === 'function') {
  // 保存延续回调,替换原始回调
  currentTask.callback = continuationCallback;
  // 任务仍在队列中,等待下次调度
}

// 步骤3:下次调度时继续执行
function workLoop(initialTime: number) {
  while (currentTask !== null) {
    const callback = currentTask.callback; // 获取当前回调
    
    if (typeof callback === 'function') {
      // 这个callback可能是:
      // 1. 原始任务回调(首次执行)
      // 2. 延续回调(恢复执行)
      
      const continuationCallback = callback(didUserCallbackTimeout);
      // 继续处理...
    }
  }
}

5. 源码实现分析

5.1 任务调度入口

function unstable_scheduleCallback(
  priorityLevel: PriorityLevel,
  callback: Callback,
  options?: {delay: number},
): Task {
  var currentTime = getCurrentTime();
  var startTime;
  if (typeof options === 'object' && options !== null) {
    var delay = options.delay;
    if (typeof delay === 'number' && delay > 0) {
      startTime = currentTime + delay;
    } else {
      startTime = currentTime;
    }
  } else {
    startTime = currentTime;
  }

    // 根据优先级计算超时时间
    var timeout;
    switch (priorityLevel) {
      case ImmediatePriority:
        timeout = -1;
        break;
      case UserBlockingPriority:
        timeout = userBlockingPriorityTimeout;
        break;
      case IdlePriority:
        timeout = maxSigned31BitInt;
        break;
      case LowPriority:
        timeout = lowPriorityTimeout;
        break;
      case NormalPriority:
      default:
        timeout = normalPriorityTimeout;
        break;
    }

    var expirationTime = startTime + timeout;

    var newTask: Task = {
      id: taskIdCounter++,
      callback,
      priorityLevel,
      startTime,
      expirationTime,
      sortIndex: -1,
    };

    if (startTime > currentTime) {
      // 延迟任务 → timerQueue
      newTask.sortIndex = startTime;
      push(timerQueue, newTask);
    } else {
      // 立即任务 → taskQueue  
      newTask.sortIndex = expirationTime;  // 按优先级排序,过期时间越早优先级越高,最小堆根据sortIndex自动排序
      push(taskQueue, newTask);
    }

    return newTask;
  }

5.2 主机环境适配

let schedulePerformWorkUntilDeadline;

if (typeof localSetImmediate === 'function') {
  // Node.js 环境:优先使用 setImmediate
  schedulePerformWorkUntilDeadline = () => {
    localSetImmediate(performWorkUntilDeadline);
  };
} else if (typeof MessageChannel !== 'undefined') {
  // 现代浏览器:使用 MessageChannel 避免 4ms 限制
  const channel = new MessageChannel();
  channel.port1.onmessage = performWorkUntilDeadline;
} else {
  // 降级方案:使用 setTimeout
  schedulePerformWorkUntilDeadline = () => {
    localSetTimeout(performWorkUntilDeadline, 0);
  };
}

6. 实际应用场景

6.1 React Fiber 中的状态保持

// React 组件渲染任务(概念性示例)
function renderComponent() {
  // 渲染部分内容...
  
  // 在 React 内部,会检查是否需要让出
  // 如果让出,会返回一个延续回调
  return null; // 完成
}

// 调度器处理(在 React 内部)
const continuationCallback = renderComponent();
if (typeof continuationCallback === 'function') {
  // 保存延续回调,下次继续执行
  // 这是调度器内部逻辑,外部代码不直接操作
}

6.2 用户交互响应场景

// 低优先级:渲染更新
unstable_scheduleCallback(LowPriority, () => {
  // 渲染大量 DOM 元素...
  
  // 如果需要让出,返回延续回调
  return () => {
    // 继续执行剩余工作
    return renderRemainingElements();
  };
});

// 高优先级:用户点击事件
unstable_scheduleCallback(UserBlockingPriority, () => {
  // 立即响应用户点击
});

// 辅助函数:渲染剩余元素
function renderRemainingElements() {
  // 继续渲染剩余的元素...
  
  // 如果还需要让出,继续返回延续回调
  // 注意:实际使用时应该通过其他方式判断是否需要让出
  if (checkTime()) {
    return () => renderRemainingElements();
  }
  return null;
}

// 辅助函数:判断是否需要让出(示例实现)
// 注意:这是一个简化的示例,实际使用时需要更复杂的逻辑
function needToYield() {
  return false; // 继续执行
}

// 更实用的示例:使用闭包保存任务开始时间
function createTaskWithTimeCheck() {
  const taskStartTime = Date.now(); // 在闭包中保存开始时间
  
  return function needToYield() {
    const currentTime = Date.now();
    if (currentTime - taskStartTime > 5) { // 5ms 时间片
      return true; // 需要让出
    }
    return false; // 继续执行
  };
}

// 使用示例
const checkTime = createTaskWithTimeCheck();
// 在任务中使用 checkTime() 来判断是否需要让出

6.3 复杂计算任务

// 低优先级:复杂计算
unstable_scheduleCallback(LowPriority, () => {
  // 执行复杂计算...
  
  // 如果需要让出,保存状态并返回延续回调
  if (checkTime()) {
    const savedState = getCurrentCalculationState();
    return () => continueCalculation(savedState);
  }
  
  return null;
});

// 辅助函数:继续计算
function continueCalculation(savedState) {
  // 使用保存的状态继续计算...
  
  if (checkTime()) {
    const newState = getCurrentCalculationState();
    return () => continueCalculation(newState);
  }
  return null;
}

// 辅助函数:获取当前计算状态
function getCurrentCalculationState() {
  // 返回当前计算的状态
  return {
    progress: 0,  // 示例值
    data: null,   // 示例值
    // ... 其他状态信息
  };
}

7. 性能优化策略

7.1 性能监控

if (enableProfiling) {
  markTaskStart(task, currentTime);      // 标记任务开始
  markTaskRun(task, currentTime);        // 标记任务运行
  markTaskYield(task, currentTime);      // 标记任务让出
  markTaskCompleted(task, currentTime);  // 标记任务完成
}

7.2 队列优化

// 使用最小堆实现任务队列,确保 O(log n) 的插入和删除
import {push, pop, peek} from '../SchedulerMinHeap';

// 任务按 sortIndex 排序,高优先级任务优先执行
var taskQueue: Array<Task> = [];  // 最小堆实现

8. 总结

8.1 核心优势

  1. 状态保持:任务执行状态不会丢失
  2. 高效恢复:从中断点继续,避免重复计算
  3. 协作式调度:任务主动控制让出时机
  4. 无缝切换:高优先级任务可以立即执行

8.2 设计原则

  • 协作式中断:任务主动让出,而非强制抢占
  • 时间切片:连续执行时间限制,确保响应性
  • 优先级排序:过期时间决定执行顺序
  • 状态保持:被中断的任务保持其执行状态
  • 自动恢复:下次调度时继续执行未完成任务

8.3 技术亮点

  • 延续回调机制:实现任务状态保持的核心
  • 双队列设计:分离立即任务和延迟任务
  • 多环境适配:自动选择最佳的主机调度API
  • 性能监控:内置性能分析支持

React Scheduler 通过这种精心设计的调度机制,实现了高性能的用户界面渲染,同时保持了良好的用户体验。它是现代前端框架中任务调度的典范,为 React 的并发特性提供了坚实的基础。


本文档基于 React 18+ 版本的 Scheduler 源码分析,源码位置: packages/scheduler/src/forks/Scheduler.js