🧑💻 前言
在上一篇文章里,我们从 scheduleUpdateOnFiber
出发,追踪到 React 内部调度的核心 ------ Scheduler。
Scheduler 是 React 的"任务大脑",负责处理优先级、队列、时间切片和可中断渲染。
本篇文章我们将从 Task 的完整生命周期 出发,结合源码拆解,理解 Scheduler 的运作原理
1. Task 完整生命周期
先整体感受一下 Task 的"生老病死":
js
Task 完整生命周期:
│
├─ 创建阶段
│ ├─ unstable_scheduleCallback(priority, callback)
│ ├─ 创建 newTask 对象
│ ├─ 计算 expirationTime
│ └─ push(taskQueue/timerQueue, newTask)
│
├─ 调度阶段
│ ├─ requestHostCallback(flushWork)
│ ├─ MessageChannel.postMessage()
│ └─ 等待下一个宏任务
│
├─ 执行阶段
│ ├─ performWorkUntilDeadline()
│ ├─ flushWork() → workLoop()
│ ├─ peek(taskQueue) 获取最高优先级任务
│ │
│ └─ while 循环消费:
│ ├─ 检查中断条件 (shouldYieldToHost)
│ ├─ 执行 task.callback(didTimeout)
│ ├─ 处理 continuationCallback
│ └─ 移除已完成任务 pop(taskQueue)
│
├─ 中断阶段 (可选)
│ ├─ shouldYieldToHost() 返回 true
│ ├─ break 退出 workLoop
│ └─ 返回 hasMoreWork = true
│
└─ 恢复阶段 (可选)
├─ 续接函数保留在 currentTask.callback
├─ 重新调度 port.postMessage()
└─ 下一个时间片继续执行
一句话总结:Task 的生命周期 = 创建 → 调度 → 执行 → 中断/恢复。
2. Task 创建阶段
2.1 入口函数:unstable_scheduleCallback
创建任务的入口是 unstable_scheduleCallback
:
js
function unstable_scheduleCallback(priorityLevel, callback, options) {
var currentTime = getCurrentTime();
// 计算任务的开始时间
var startTime = currentTime;
if (options && typeof options.delay === 'number' && options.delay > 0) {
startTime = currentTime + options.delay;
}
// 根据优先级确定超时时间
var timeout;
switch (priorityLevel) {
case ImmediatePriority: timeout = -1; break;
case UserBlockingPriority: timeout = 250; break;
case LowPriority: timeout = 10000; break;
case IdlePriority: timeout = IDLE_PRIORITY_TIMEOUT; break;
case NormalPriority:
default: timeout = 5000; break;
}
var expirationTime = startTime + timeout;
// 构建任务对象
var newTask = {
id: taskIdCounter++,
callback,
priorityLevel,
startTime,
expirationTime,
sortIndex: -1,
};
// 判断任务进入哪个队列
if (startTime > currentTime) {
newTask.sortIndex = startTime;
push(timerQueue, newTask);
requestHostTimeout(handleTimeout, startTime - currentTime);
} else {
newTask.sortIndex = expirationTime;
push(taskQueue, newTask);
requestHostCallback(flushWork);
}
return newTask;
}
2.2 Task 对象结构
js
var newTask = {
id: taskIdCounter++, // 唯一任务ID
callback, // 要执行的回调函数
priorityLevel, // 优先级
startTime, // 任务开始时间
expirationTime, // 过期时间
sortIndex: expirationTime// 堆排序依据
};
2.3 原理解析
Scheduler 的核心目标是:把任务拆成小块,分时间片执行,保证 UI 不被长任务卡住。
在任务创建阶段:
-
计算
startTime
:如果有delay
,任务是延迟执行的,放入timerQueue
。 -
计算
expirationTime
:不同优先级对应不同超时时间:- Immediate:马上执行
- UserBlocking:250ms 内完成
- Normal:5000ms
- Low:10000ms
- Idle:空闲时执行
-
放入队列:
- 延迟任务 →
timerQueue
- 可立即执行任务 →
taskQueue
- 延迟任务 →
-
调度触发:
requestHostCallback(flushWork)
→ 浏览器下一个宏任务触发执行- 延迟任务设置定时器 → 时间到移入
taskQueue
3. 队列管理
Scheduler 内部维护两个最小堆:
js
var taskQueue = []; // 按 expirationTime 排序
var timerQueue = []; // 按 startTime 排序
3.1 延迟任务转移机制
当时间到达时,延时任务会从 timerQueue
移入 taskQueue
:
js
function advanceTimers(currentTime) {
let timer = peek(timerQueue);
while (timer !== null) {
if (timer.callback === null) {
pop(timerQueue); // 已取消
} else if (timer.startTime <= currentTime) {
pop(timerQueue);
timer.sortIndex = timer.expirationTime;
push(taskQueue, timer);
} else {
return; // 还没到时间
}
timer = peek(timerQueue);
}
}
📌 原理 :
延迟任务不会阻塞主线程,时间到后才进入执行队列。
4. Task 执行阶段
4.1 主循环:workLoop
js
function workLoop(hasTimeRemaining, initialTime) {
let currentTime = initialTime;
advanceTimers(currentTime);
currentTask = peek(taskQueue);
while (currentTask !== null) {
if (
currentTask.expirationTime > currentTime &&
(!hasTimeRemaining || shouldYieldToHost())
) {
break;
}
const callback = currentTask.callback;
currentTask.callback = null;
const didTimeout = currentTask.expirationTime <= currentTime;
const continuationCallback = callback(didTimeout);
currentTime = getCurrentTime();
if (typeof continuationCallback === 'function') {
currentTask.callback = continuationCallback;
} else {
if (currentTask === peek(taskQueue)) {
pop(taskQueue);
}
}
advanceTimers(currentTime);
currentTask = peek(taskQueue);
}
return currentTask !== null;
}
4.2 执行逻辑总结
peek(taskQueue)
取最高优先级任务- 检查是否超时、是否需要让出主线程
- 执行
task.callback(didTimeout)
- 有续接函数 → 保存回调,下次继续
- 无续接函数 → 任务完成,移出队列
📌 原理 :
时间切片机制(默认 5ms)保证浏览器主线程不会被长任务阻塞。
5. Task 中断机制
中断点在 workLoop
:
js
if (
currentTask.expirationTime > currentTime &&
(!hasTimeRemaining || shouldYieldToHost())
) {
break; // 中断
}
5.1 shouldYieldToHost
js
function shouldYieldToHost() {
const currentTime = getCurrentTime();
if (currentTime >= deadline) {
if (needsPaint || (navigator.scheduling && navigator.scheduling.isInputPending())) {
return true;
}
return currentTime >= maxYieldInterval; // 最多 300ms
}
return false;
}
📌 原理:
- 每个时间片最多 5ms(短任务)
- 遇到用户交互或绘制请求立即中断
- 避免长任务阻塞 UI
6. Task 恢复机制
如果任务没完成,回调会返回一个 续接函数:
js
const continuationCallback = callback(didTimeout);
if (typeof continuationCallback === 'function') {
currentTask.callback = continuationCallback;
}
Scheduler 会在下一个时间片继续执行。
React 渲染任务正是利用这个机制实现 可中断渲染:
js
function performConcurrentWorkOnRoot(root, didTimeout) {
if (workInProgress !== null) {
return performConcurrentWorkOnRoot.bind(null, root);
}
return null;
}
7. 取消机制
取消任务非常简单:
js
function unstable_cancelCallback(task) {
task.callback = null;
}
在 workLoop
执行时会跳过被取消的任务。
8. 完整时序图
js
用户调用 unstable_scheduleCallback
│
├─ 创建 Task
│ ├─ 计算 startTime / expirationTime
│ └─ 放入 taskQueue / timerQueue
│
├─ 调度
│ └─ requestHostCallback(flushWork)
│ └─ MessageChannel.postMessage()
│
├─ 宿主环境触发 performWorkUntilDeadline
│ └─ flushWork()
│ └─ workLoop()
│ ├─ advanceTimers()
│ ├─ 取出 taskQueue 队头
│ ├─ shouldYieldToHost → 时间切片判断
│ ├─ 执行 callback(didTimeout)
│ ├─ continuationCallback → 保存续接
│ └─ pop 已完成任务
│
├─ 中断 (可选)
│ └─ shouldYieldToHost 返回 true → break
│
└─ 恢复 (可选)
├─ 续接函数保存在 currentTask.callback
├─ 再次 requestHostCallback
└─ 下一个时间片继续执行
✅ 总结
Scheduler 的核心机制:
- 任务优先级:Immediate > UserBlocking > Normal > Low > Idle
- 双队列管理:taskQueue + timerQueue
- 时间切片:默认 5ms,最大 300ms
- 可中断/恢复:续接函数机制
- 任务取消:callback = null
这套机制保证 React 在浏览器主线程上能够 流畅渲染,不阻塞交互。