Day 6 核心内容:调度器原理

9 阅读6分钟

React 源码面试冲刺 - Day 6

日期:2026-03-23 主题:调度器原理


📖 Day 6 核心内容:调度器原理

👴 老大爷能听懂版

调度器 = React 的"小和时间管理大师"

场景旧方式调度器方式
渲染大列表一直渲染,卡死分批渲染,插空做
鼠标移动反应慢空闲时处理
用户输入阻塞优先响应

调度器的核心:不要一次干完!

旧版本:我要搬家,一口气搬完,累死
新版本:
  - 搬一阵,歇一歇
  - 有人敲门(用户输入),先去开门
  - 搬完再歇

两个关键概念:

  1. 时间片(Time Slice) = 每段时间,比如 5ms
  2. 空闲回调(Idle Callback) = 有空时再干

💻 专业开发者版

React 调度器的核心 API:

// 调度的关键函数
function scheduleCallback(priority, callback) {
  // 把任务放进队列
}

function shouldYield() {
  // 检查是否要让出主线程
}

// 使用方式
function task() {
  doWork(); // 干一点
  
  if (!shouldYield()) {
    task(); // 继续干
  } else {
    requestIdleCallback(task); // 让出,下次继续
  }
}

React 的任务优先级:

// scheduler/src PriorityLevels.js
const ImmediatePriority = 1;    // 最高:setTimeout 0
const UserBlockingPriority = 2; // 用户阻塞:输入、滚动
const NormalPriority = 3;       // 普通:网络请求
const LowPriority = 4;          // 低:预加载
const IdlePriority = 5;         // 最低:后台任务

调度器的工作流程:

用户触发更新
     ↓
分配优先级
     ↓
加入任务队列
     ↓
┌─────────────────┐
│ Event Loop      │
│  每帧 16.67ms   │
├─────────────────┤
│ 处理 DOM 事件   │ ← 及时响应用户
│ 调用 requestAnimationFrame
│ 执行调度任务    │ ← 用 shouldYield() 控制
│ 空闲时执行      │ ← 用 requestIdleCallback
└─────────────────┘

时间片机制:

// React 用 MessageChannel 模拟时间片
const channel = new MessageChannel();
const port = channel.port1;

function workLoop() {
  while (nextUnitOfWork && !shouldYield()) {
    // 处理一个任务单元
    nextUnitOfWork = performUnitOfWork(nextUnitOfWork);
  }
  
  // 没处理完,预约下次
  if (pendingCallback) {
    port.postMessage(null); // 触发下一帧
  }
}

channel.port2.onmessage = workLoop;
port.postMessage(null); // 开始

shouldYield() 原理:

let currentTime;

// 检查是否要让出
function shouldYield() {
  // 当前帧剩余时间
  const timeRemaining = getCurrentTime() - currentTime;
  
  // 如果剩余时间 < 0,让出主线程
  // 0ms 表示必须让出
  return timeRemaining < 0;
}

// 每帧开始时
function flushWork() {
  currentTime = getCurrentTime();
  
  // 处理所有可以执行的任务
  workLoop();
}

任务队列管理:

// 调度器维护多个队列
let taskQueue = [];      // 逾期任务(必须执行)
let timerQueue = [];     // 未到期任务(等时间)

// 添加任务
function scheduleCallback(callback) {
  const currentTime = getCurrentTime();
  const timeout = priorityToTimeout[priority];
  const expiryTime = currentTime + timeout;
  
  const task = {
    callback,
    expiryTime,
    priority,
    sortIndex: expiryTime,
  };
  
  // 根据过期时间排序
  if (expiryTime < timerQueue[0]?.expiryTime) {
    // 是最早过期的���优先处理
  }
  
  requestHostCallback();
}

React 的调度策略:

  1. 高优先级插队

    正在渲染列表 → 用户点击按钮
        ↓
    按钮优先级更高,中断渲染
        ↓
    处理按钮点击 → 更新 UI
        ↓
    继续渲染列表
    
  2. 批量更新

    setState 触发 3 次
        ↓
    调度器合并为 1 次渲染
        ↓
    只渲染 1 次!
    
  3. 饥饿问题解决

    低优先级任务一直被打断 → 饿到了
        ↓
    设置 timeout,过期必须执行
        ↓
    过期任务优先处理
    

💪 面试高频问题

问题答案要点
React 调度器是什么?管理任务优先级和执行时机的模块
什么是时间片?把任务拆成小块,每块 5ms,到点就让
shouldYield 怎么实现?检查当前帧剩余时间,<0 就让出
为什么需要空闲回调?处理不紧急的任务,不影响用户交互
高优先级怎么插队?中断当前任务,先执行高优先级
什么是饥饿问题?低优先级一直被高优先级打断,永远轮不到
如何解决?设置 timeout,过期必须执行

📊 Day 1-6 面试能力评估

Day 1-6 学完能答:

  • ✅ React 核心 API
  • ✅ JSX 转换原理
  • ✅ Hooks 原理
  • ✅ Fiber 架构
  • ✅ Diff 算法
  • 调度器原理 ← 今日新增

还需要 Day 7+:

  • ❌ 并发模式
  • ❌ 状态管理原理

💪 今日自测

  1. 调度器的核心作用是什么?
  2. shouldYield() 是怎么判断要不要让出主线程的?
  3. 高优先级任务如何"插队"?
  4. 什么是"饥饿问题",怎么解决的?

📝 详细答案

1. 调度器的核心作用是什么?

核心:管理任务执行时机,让页面不卡顿

没有调度器:
- 渲染大列表 → 一直渲染 → 页面卡死 16 秒
- 用户无法输入 → 点了没反应

有调度器:
- 渲染 1000 条 → 每批 20 条,分 50 批
- 每批 5ms,总共 250ms
- 中间可以响应用户输入 → 流畅!

具体作用:

作用说明
拆分任务把大任务拆成小片段
优先级紧急任务先做
让出主线程干一会歇一会
批量更新多次 setState 合并成一次渲染

2. shouldYield() 是怎么判断要不要让出主线程的?

原理:检查当前帧还剩多少时间

// React 的实现(简化)
let currentTime = 0;
let frameDeadline = 0;

// 每帧开始时记录截止时间
function scheduleFrame(deadline) {
  currentTime = deadline;
  frameDeadline = deadline + 5; // 5ms 时间片
}

// shouldYield 检查是否到期
function shouldYield() {
  // 当前时间 > 截止时间 → 让出
  return getCurrentTime() >= frameDeadline;
}

实际流程:

┌─────────────────────────────────────────┐
│  浏览器帧 (16.67ms = 60fps)             │
│  ┌────────────────────┐                 │
│  │ 用户事件处理       │ ← 高优,优先处理 │
│  ├────────────────────┤                 │
│  │ rAF (动画帧)       │                 │
│  ├────────────────────┤                 │
│  │ React 渲染任务     │ ← 用 shouldYield │
│  │ ████░░░░░░░       │   控制,每5ms检查  │
│  ├────────────────────┤                 │
│  │ Idle 空闲任务      │ ← requestIdleCallback │
│  └────────────────────┘                 │
└─────────────────────────────────────────┘

为什么是 5ms?

  • 帧率 60fps = 每帧 16.67ms
  • 留 5ms 给浏览器做其他事
  • 11ms 给 React 安全使用

3. 高优先级任务如何"插队"?

核心:中断当前任务,优先执行新的

// 场景:正在渲染列表,用户点击按钮

// 1. 渲染列表(NormalPriority)
function renderList() {
  while (items.length > 0) {
    processItem(items.pop());
    
    // 2. 检查是否有更高优先级的任务
    if (shouldYield()) {
      // 3. 有高优任务?直接返回,让出主线程
      return renderList; // 下次继续
    }
  }
}

// 用户点击触发更新:
document.addEventListener('click', () => {
  // 立即分配最高优先级
  scheduleCallback(ImmediatePriority, () => {
    updateButton();
  });
});

// 调度器检测到高优先级:
// - 停止当前 renderList
// - 优先执行 updateButton
// - 执行完再继续 renderList

具体实现:

// React 的优先级比较
function getPriorityLevel(current, incoming) {
  // 如果新任务优先级更高,返回新任务
  return incoming.priority < current.priority ? incoming : current;
}

效果展示:

时间线:
─────────────────────────────────────────►

[渲染列表... ████████░░] 用户点击!
              ↓ 中断
[处理按钮 ●●●●●●●●] ← 插队
              ↓ 完成
[继续渲染列表 ████░░░] ← 接着干

4. 什么是"饥饿问题",怎么解决的?

饥饿问题:低优先级一直被高优先级打断,永远轮不到

场景:
- 用户疯狂点击(ImmediatePriority)
- 后台在渲染一个大列表(LowPriority)

结果:
- 每次点击都打断渲染
- 列表永远渲染不完 → 饿死!

React 的解决方案:设置过期时间(timeout)

// 设置不同优先级的过期时间
const priorityToTimeout = {
  ImmediatePriority: -1,   // 立即执行
  UserBlockingPriority: 250,  // 250ms
  NormalPriority: 5000,    // 5秒
  LowPriority: 10000,      // 10秒
  IdlePriority: 10000,     // 10秒
};

过期机制:

场景:低优先级任务被饿死

1. 任务 A (LowPriority) 加入队列
2. 用户不断点击,触发高优先任务
3. A 等待中... 等待中... 等待中...
4. 10 秒后 A 过期了!

5. 调度器检查:A 的 expiryTime < 当前时间
6. 把 A 移到 taskQueue(必须执行)
7. 下次tick 优先执行过期的 A

代码逻辑:

function flushWork() {
  // 1. 找出所有过期的任务
  let expiredItems = [];
  
  while (taskQueue.length > 0) {
    const task = peek(taskQueue);
    
    // 2. 检查是否过期
    if (task.expiryTime <= currentTime) {
      expiredItems.push(task);
      pop(taskQueue);
    } else {
      break; // 没过期,停止
    }
  }
  
  // 3. 优先处理过期任务
  if (expiredItems.length > 0) {
    return sortByExpirationTime(expiredItems);
  }
}

总结:过期时间 = 防止饥饿的保险

优先级过期时间含义
Immediate-1必须立即执行
UserBlocking250ms用户能感知的最长时间
Normal5s网络请求超时时间
Low10s可以等的底线

Day 6 ✅ 完成!调度器核心:时间片 + 优先级 + 过期时间 = 流畅体验!

明天 Day 7,并发模式 🚀