React 19 源码揭秘(六):Scheduler 时间切片的秘密
本文深入 Scheduler 源码,揭秘 React 如何实现可中断渲染和优先级调度。
前言
你有没有想过:
- React 是怎么做到"不卡顿"的?
- 用户输入为什么能打断正在进行的渲染?
- 时间切片到底是什么?
答案就在 Scheduler 调度器中。
一、为什么需要 Scheduler?
问题:长任务阻塞
// 假设有 10000 个节点需要渲染
function render() {
for (let i = 0; i < 10000; i++) {
processNode(i); // 每个节点处理 1ms
}
}
// 总共需要 10 秒,期间页面完全卡死!
解决:时间切片
function render() {
let i = 0;
function work() {
while (i < 10000 && !shouldYield()) { // 每 5ms 检查一次
processNode(i++);
}
if (i < 10000) {
scheduleCallback(work); // 还有任务,继续调度
}
}
scheduleCallback(work);
}
// 每 5ms 让出主线程,用户可以正常交互
二、核心概念
优先级
// SchedulerPriorities.js
export const NoPriority = 0;
export const ImmediatePriority = 1; // 立即执行
export const UserBlockingPriority = 2; // 用户交互(250ms)
export const NormalPriority = 3; // 普通更新(5000ms)
export const LowPriority = 4; // 低优先级(10000ms)
export const IdlePriority = 5; // 空闲执行(永不过期)
超时时间
不同优先级有不同的超时时间:
var timeout;
switch (priorityLevel) {
case ImmediatePriority:
timeout = -1; // 立即过期
break;
case UserBlockingPriority:
timeout = 250; // 250ms
break;
case NormalPriority:
timeout = 5000; // 5 秒
break;
case LowPriority:
timeout = 10000; // 10 秒
break;
case IdlePriority:
timeout = 1073741823; // 约 12 天,几乎永不过期
break;
}
Task 结构
type Task = {
id: number, // 任务 ID
callback: Function, // 任务回调
priorityLevel: number, // 优先级
startTime: number, // 开始时间
expirationTime: number, // 过期时间 = startTime + timeout
sortIndex: number, // 排序索引
};
三、两个队列
Scheduler 维护两个队列:
var taskQueue = []; // 就绪队列(按 expirationTime 排序)
var timerQueue = []; // 延迟队列(按 startTime 排序)
┌─────────────────────────────────────────────────────────┐
│ timerQueue │
│ (延迟任务) │
│ │
│ startTime > currentTime 的任务在这里等待 │
└─────────────────────────────────────────────────────────┘
│
│ 到时间了
▼
┌─────────────────────────────────────────────────────────┐
│ taskQueue │
│ (就绪任务) │
│ │
│ 按 expirationTime 排序,最小堆实现 │
└─────────────────────────────────────────────────────────┘
│
│ 取出执行
▼
执行任务回调
四、最小堆
任务队列使用最小堆实现,保证 O(1) 获取最高优先级任务:
// SchedulerMinHeap.js
export function push(heap, node) {
const index = heap.length;
heap.push(node);
siftUp(heap, node, index); // 上浮
}
export function peek(heap) {
return heap.length === 0 ? null : heap[0]; // O(1)
}
export function pop(heap) {
const first = heap[0];
const last = heap.pop();
if (last !== first) {
heap[0] = last;
siftDown(heap, last, 0); // 下沉
}
return first;
}
// 比较函数:先比 sortIndex,再比 id
function compare(a, b) {
const diff = a.sortIndex - b.sortIndex;
return diff !== 0 ? diff : a.id - b.id;
}
五、调度流程
scheduleCallback
function unstable_scheduleCallback(priorityLevel, callback, options) {
var currentTime = getCurrentTime();
var startTime = currentTime;
// 计算过期时间
var timeout = getTimeoutByPriority(priorityLevel);
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);
// 请求调度
requestHostCallback();
}
return newTask;
}
workLoop
function workLoop(initialTime) {
let currentTime = initialTime;
// 检查延迟任务是否到期
advanceTimers(currentTime);
// 获取最高优先级任务
currentTask = peek(taskQueue);
while (currentTask !== null) {
// 任务未过期 && 时间片用完 → 中断
if (currentTask.expirationTime > currentTime && shouldYieldToHost()) {
break;
}
const callback = currentTask.callback;
if (typeof callback === 'function') {
currentTask.callback = null;
// 执行任务
const didUserCallbackTimeout = currentTask.expirationTime <= currentTime;
const continuationCallback = callback(didUserCallbackTimeout);
if (typeof continuationCallback === 'function') {
// 任务未完成,保留继续执行
currentTask.callback = continuationCallback;
return true;
} else {
// 任务完成,移除
if (currentTask === peek(taskQueue)) {
pop(taskQueue);
}
}
} else {
pop(taskQueue);
}
currentTask = peek(taskQueue);
}
return currentTask !== null;
}
shouldYieldToHost
let frameInterval = 5; // 5ms 一个时间片
let startTime = -1;
function shouldYieldToHost() {
const timeElapsed = getCurrentTime() - startTime;
if (timeElapsed < frameInterval) {
return false; // 时间片未用完,继续
}
return true; // 让出主线程
}
六、调度触发:MessageChannel
为什么用 MessageChannel 而不是 setTimeout?
// setTimeout 有最小 4ms 延迟
setTimeout(callback, 0); // 实际 >= 4ms
// MessageChannel 没有这个限制
const channel = new MessageChannel();
channel.port1.onmessage = callback;
channel.port2.postMessage(null); // 立即触发
实现
const channel = new MessageChannel();
const port = channel.port2;
channel.port1.onmessage = performWorkUntilDeadline;
function schedulePerformWorkUntilDeadline() {
port.postMessage(null);
}
function performWorkUntilDeadline() {
if (isMessageLoopRunning) {
const currentTime = getCurrentTime();
startTime = currentTime;
let hasMoreWork = true;
try {
hasMoreWork = flushWork(currentTime);
} finally {
if (hasMoreWork) {
// 还有任务,继续调度
schedulePerformWorkUntilDeadline();
} else {
isMessageLoopRunning = false;
}
}
}
}
七、与 React 的关系
React 更新
│
▼
scheduleUpdateOnFiber()
│
▼
ensureRootIsScheduled()
│
▼
scheduleSyncCallback() 或 scheduleCallback()
│
▼
Scheduler.scheduleCallback(priority, performConcurrentWorkOnRoot)
│
▼
workLoop() → performConcurrentWorkOnRoot()
│
▼
workLoopConcurrent() ← 这里会检查 shouldYield()
workLoopConcurrent
function workLoopConcurrent() {
while (workInProgress !== null && !shouldYield()) {
performUnitOfWork(workInProgress);
}
}
当 shouldYield() 返回 true 时,React 会暂停渲染,让出主线程。
八、优先级抢占
高优先级任务可以打断低优先级任务:
// 正在进行低优先级渲染
scheduleCallback(NormalPriority, renderLowPriority);
// 用户点击,触发高优先级更新
scheduleCallback(UserBlockingPriority, renderHighPriority);
// workLoop 中:
// 1. 高优先级任务先过期
// 2. 取出高优先级任务执行
// 3. 低优先级任务被"饿死"后也会执行
九、调试技巧
// 在这些位置打断点:
// 调度任务
unstable_scheduleCallback // Scheduler.js
// 执行任务
workLoop // Scheduler.js
// 时间切片检查
shouldYieldToHost // Scheduler.js
// 调度触发
performWorkUntilDeadline // Scheduler.js
观察时间切片
// 在 workLoopConcurrent 中加日志
function workLoopConcurrent() {
let count = 0;
while (workInProgress !== null && !shouldYield()) {
performUnitOfWork(workInProgress);
count++;
}
console.log(`处理了 ${count} 个节点后让出`);
}
小结
Scheduler 的核心机制:
- 优先级:5 个级别,不同超时时间
- 两个队列:taskQueue(就绪)+ timerQueue(延迟)
- 最小堆:O(1) 获取最高优先级任务
- 时间切片:5ms 一片,shouldYield 检查
- MessageChannel:避免 setTimeout 的 4ms 延迟
- 可中断:高优先级可以打断低优先级
📦 配套源码:github.com/220529/reac…
上一篇:Diff 算法的实现
如果觉得有帮助,欢迎点赞收藏 👍