React 源码面试冲刺 - Day 6
日期:2026-03-23 主题:调度器原理
📖 Day 6 核心内容:调度器原理
👴 老大爷能听懂版
调度器 = React 的"小和时间管理大师"
| 场景 | 旧方式 | 调度器方式 |
|---|---|---|
| 渲染大列表 | 一直渲染,卡死 | 分批渲染,插空做 |
| 鼠标移动 | 反应慢 | 空闲时处理 |
| 用户输入 | 阻塞 | 优先响应 |
调度器的核心:不要一次干完!
旧版本:我要搬家,一口气搬完,累死
新版本:
- 搬一阵,歇一歇
- 有人敲门(用户输入),先去开门
- 搬完再歇
两个关键概念:
- 时间片(Time Slice) = 每段时间,比如 5ms
- 空闲回调(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 的调度策略:
-
高优先级插队
正在渲染列表 → 用户点击按钮 ↓ 按钮优先级更高,中断渲染 ↓ 处理按钮点击 → 更新 UI ↓ 继续渲染列表 -
批量更新
setState 触发 3 次 ↓ 调度器合并为 1 次渲染 ↓ 只渲染 1 次! -
饥饿问题解决
低优先级任务一直被打断 → 饿到了 ↓ 设置 timeout,过期必须执行 ↓ 过期任务优先处理
💪 面试高频问题
| 问题 | 答案要点 |
|---|---|
| React 调度器是什么? | 管理任务优先级和执行时机的模块 |
| 什么是时间片? | 把任务拆成小块,每块 5ms,到点就让 |
| shouldYield 怎么实现? | 检查当前帧剩余时间,<0 就让出 |
| 为什么需要空闲回调? | 处理不紧急的任务,不影响用户交互 |
| 高优先级怎么插队? | 中断当前任务,先执行高优先级 |
| 什么是饥饿问题? | 低优先级一直被高优先级打断,永远轮不到 |
| 如何解决? | 设置 timeout,过期必须执行 |
📊 Day 1-6 面试能力评估
Day 1-6 学完能答:
- ✅ React 核心 API
- ✅ JSX 转换原理
- ✅ Hooks 原理
- ✅ Fiber 架构
- ✅ Diff 算法
- ✅ 调度器原理 ← 今日新增
还需要 Day 7+:
- ❌ 并发模式
- ❌ 状态管理原理
💪 今日自测
- 调度器的核心作用是什么?
- shouldYield() 是怎么判断要不要让出主线程的?
- 高优先级任务如何"插队"?
- 什么是"饥饿问题",怎么解决的?
📝 详细答案
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 | 必须立即执行 |
| UserBlocking | 250ms | 用户能感知的最长时间 |
| Normal | 5s | 网络请求超时时间 |
| Low | 10s | 可以等的底线 |
Day 6 ✅ 完成!调度器核心:时间片 + 优先级 + 过期时间 = 流畅体验!
明天 Day 7,并发模式 🚀