React Scheduler 调度器
这篇笔记重点学的不是“背调度器源码”,而是理解 React 为什么需要调度、调度分成哪两层、一次更新如何从 root 上的 lane 走到浏览器任务队列,以及 render 为什么能让步而 commit 为什么必须原子完成。
React 需要 Scheduler 的根本原因是:浏览器主线程同时负责 JS 执行、事件响应、样式计算、布局和绘制。如果 React render 一次性递归处理大组件树,就可能形成长任务,阻塞点击、输入、滚动和下一帧绘制。Scheduler 的目标不是让 React 变成多线程,而是在主线程上做协作式调度:
高优先级更新尽快响应
低优先级 render 可以切片、让步、恢复或重来
commit 阶段保持一次性提交,避免 UI 半更新
学习时要先把 React 里的两层 Scheduler 分清楚:
层次 | 位置 | 关注点 | 是否理解 React 语义 |
|---|---|---|---|
Root Scheduler |
| 哪个 root 有工作、选哪些 lanes、同步 flush 还是并发 callback、callback 复用/取消 | 是 |
|
| task 队列、延迟任务、优先级超时、MessageChannel、时间切片、continuation | 否 |
整体链路可以先记成:
setState / dispatch
-> requestUpdateLane
-> update 入队
-> scheduleUpdateOnFiber
-> markRootUpdated(root, lane)
-> ensureRootIsScheduled(root)
-> microtask: processRootScheduleInMicrotask
-> scheduleTaskForRootDuringMicrotask
-> SyncLane: microtask 末尾同步 flush
-> 非 SyncLane: scheduleCallback 到 scheduler package
-> MessageChannel 驱动 performWorkUntilDeadline
-> flushWork / workLoop
-> React renderRootConcurrent 可 yield
-> render 完成后 commitRoot 原子提交一、整体认知:Scheduler 解决主线程响应性问题
React 更新不是孤立地“改某个组件”。组件触发更新后,React 最终要从 FiberRoot 出发,根据 lane、update queue、Suspense 状态、Fiber 树和当前 WIP 进度计算一棵新的树。这个计算可能很重,因此需要调度。
浏览器主线程上的工作大致包括:
工作 | 说明 |
|---|---|
JS 执行 | React render、事件回调、业务逻辑 |
事件响应 | click、input、scroll、pointermove |
样式与布局 | style recalculation、layout |
绘制与合成 | paint、composite |
微任务处理 | Promise、queueMicrotask |
如果 React 长时间占住主线程,用户输入会排队,浏览器也没有机会 paint。Scheduler 解决的是响应性,不是单次更新的绝对计算量:
没有 Scheduler:
大 render 一口气跑完
-> 主线程被 JS 长任务占住
-> 点击/输入/绘制都延迟
有 Scheduler:
更新先按 lane 区分紧急程度
-> 紧急更新优先
-> 非紧急 render 分片执行
-> 时间片到了主动让出主线程这里的“主动让出”很重要。React 调度是协作式调度,不是浏览器强制抢占正在运行的 JS。React 能让步,是因为 Fiber 把 render 拆成很多工作单元,work loop 可以在单元之间检查 shouldYield()。
整体关系图:
flowchart TD
A["用户事件 / setState"] --> B["创建 update"]
B --> C["选择 lane"]
C --> D["update 入队"]
D --> E["root.pendingLanes 标记更新"]
E --> F["Root Scheduler 选择 root 和 lanes"]
F --> G{"SyncLane?"}
G -- "是" --> H["microtask 末尾同步 flush"]
G -- "否" --> I["scheduleCallback"]
I --> J["scheduler package"]
J --> K["MessageChannel host callback"]
K --> L["React concurrent render"]
L --> M{"shouldYield?"}
M -- "是" --> N["返回 continuation,下一轮继续"]
M -- "否" --> O["render 完成"]
H --> O
O --> P["commitRoot 原子提交"]一句话总结:
Root Scheduler 决定 React 要做什么工作;
scheduler package 决定这段 callback 在浏览器主线程上怎么分片执行。接下来先看第一层:React 自己如何把多个组件更新收拢到 root 维度。
二、第一层模型:Root Scheduler 管 root、lane 和同步分流
Root Scheduler 是 React reconciler 内部的策略层。它理解 FiberRoot、pendingLanes、suspendedLanes、pingedLanes、expiredLanes、callbackNode 和 callbackPriority。
它主要回答这些问题:
这个 root 是否已经进入调度系统?
下一批应该处理哪些 lanes?
这批 lanes 是同步 flush,还是交给 scheduler package?
已有 callback 能不能复用?
旧 callback 要不要取消?
render yield 后是否需要 continuation?2.1 从 setState 到 root.pendingLanes
一次普通 Hook 更新可以压缩成:
setState
-> requestUpdateLane
-> 创建 update
-> enqueue update
-> scheduleUpdateOnFiber(root, fiber, lane)
-> markRootUpdated(root, lane)
-> ensureRootIsScheduled(root)简化版源码:
function dispatchSetState(fiber, queue, action) {
const lane = requestUpdateLane(fiber);
const update = { lane, action, next: null };
const root = enqueueConcurrentHookUpdate(fiber, queue, update, lane);
if (root !== null) {
scheduleUpdateOnFiber(root, fiber, lane);
}
}function scheduleUpdateOnFiber(root, fiber, lane) {
markRootUpdated(root, lane);
ensureRootIsScheduled(root);
}markRootUpdated(root, lane) 的核心效果是:
root.pendingLanes |= lanependingLanes 不是 FIFO 队列,而是 bitmask 集合:
root.pendingLanes = SyncLane | DefaultLane | TransitionLane1三个概念要分开:
概念 | 说明 |
|---|---|
update queue | 保存具体状态更新对象 |
lane | update 的优先级和批次标签 |
| root 级别还有哪些优先级待处理 |
所以 setState 不是立刻改 DOM,而是创建 update、标记 lane、把 root 放进调度系统。真正的新 state 会在 render 阶段消费 update queue 时计算。
2.2 root schedule 链表
Root Scheduler 用全局单链表记录“哪些 root 需要被调度”:
字段 | 含义 |
|---|---|
| 链表头 |
| 链表尾,用于 O(1) 追加 |
| 指向下一个 scheduled root,也用于判断 root 是否已入链 |
ensureRootIsScheduled(root) 的简化逻辑:
function ensureRootIsScheduled(root) {
if (root === lastScheduledRoot || root.next !== null) {
// 已经在 schedule 链表里,不重复入链
} else if (lastScheduledRoot === null) {
firstScheduledRoot = lastScheduledRoot = root;
} else {
lastScheduledRoot.next = root;
lastScheduledRoot = root;
}
mightHavePendingSyncWork = true;
ensureScheduleIsScheduled();
}同一个事件里多次 setState 时:
多次 update 入队
-> 多次 markRootUpdated(root, lane)
-> root.pendingLanes 累积 lanes
-> root schedule 链表里仍然只有这个 root
-> didScheduleMicrotask 保证通常只安排一个 microtask这就是 React 合并调度的一个关键点:合并发生在 root/lane 调度层,不是把 update 对象简单丢掉。
2.3 callbackNode 与 callbackPriority
每个 root 上还有两个字段,用来复用或取消底层 scheduler task:
字段 | 含义 |
|---|---|
| 已经投递到底层 scheduler package 的 task node;同步任务通常是 |
| 当前 callback 对应的 React lane 优先级,不是 scheduler priority |
scheduleTaskForRootDuringMicrotask 会比较:
existingCallbackPriority = root.callbackPriority
newCallbackPriority = getHighestPriorityLane(nextLanes)如果新旧优先级一样,可以复用旧 callback;如果优先级变了,就取消旧 callback 并重新安排。这也是为什么连续多次同优先级并发更新,不一定会创建多个底层 scheduler task。
2.4 Root Scheduler 的核心链路
第一层完整链路:
flowchart TD
A["setState"] --> B["requestUpdateLane"]
B --> C["enqueue update"]
C --> D["scheduleUpdateOnFiber"]
D --> E["markRootUpdated: pendingLanes |= lane"]
E --> F["ensureRootIsScheduled"]
F --> G{"root 已在链表中?"}
G -- "否" --> H["追加到 firstScheduledRoot / lastScheduledRoot"]
G -- "是" --> I["复用链表节点"]
H --> J["ensureScheduleIsScheduled"]
I --> J
J --> K["scheduleImmediateRootScheduleTask"]
K --> L["microtask"]
L --> M["processRootScheduleInMicrotask"]
M --> N["scheduleTaskForRootDuringMicrotask"]
N --> O["getNextLanes"]
O --> P{"包含 SyncLane?"}
P -- "是" --> Q["microtask 末尾 flushSyncWorkAcrossRoots_impl"]
P -- "否" --> R["scheduleCallback 到 scheduler package"]到这里为止,React 还没有进入底层 scheduler package。它只是把 root 收拢起来,并准备在一个 microtask 中统一做分流。
三、Microtask 层:入队、遍历 root schedule 与同步 flush
Root Scheduler 入链后不会立刻打断当前 JS 调用栈。它通过 microtask 在当前事件回调结束后尽快处理 root schedule。
3.1 microtask 如何创建
ensureScheduleIsScheduled() 用 didScheduleMicrotask 去重:
function ensureScheduleIsScheduled() {
if (!didScheduleMicrotask) {
didScheduleMicrotask = true;
scheduleImmediateRootScheduleTask();
}
}DOM 环境里的 scheduleMicrotask 大致按这个顺序选择:
queueMicrotask
-> Promise.resolve().then
-> setTimeout fallback简化入口:
function scheduleImmediateRootScheduleTask() {
scheduleMicrotask(() => {
processRootScheduleInMicrotask();
});
}这意味着同一轮事件中,即使多个 root 或多个 update 反复触发 ensureRootIsScheduled,通常也只会安排一个处理 root schedule 的 microtask。
3.2 processRootScheduleInMicrotask 做什么
processRootScheduleInMicrotask() 是 microtask 里的主入口:
function processRootScheduleInMicrotask() {
didScheduleMicrotask = false;
let root = firstScheduledRoot;
while (root !== null) {
const next = root.next;
const nextLanes = scheduleTaskForRootDuringMicrotask(root, now());
if (nextLanes === NoLane) {
removeRootFromSchedule(root);
}
root = next;
}
flushSyncWorkAcrossRoots_impl();
}它做三件事:
重置
didScheduleMicrotask,允许后续事件再投递新的 root schedule microtask。遍历
firstScheduledRoot -> root.next链表,对每个 root 调用scheduleTaskForRootDuringMicrotask。在 microtask 末尾统一调用
flushSyncWorkAcrossRoots_impl处理同步工作。
这里的“末尾 flush”很关键:
processRootScheduleInMicrotask
-> 先遍历所有 scheduled roots
-> SyncLane root 只标记 callbackPriority = SyncLane,callbackNode = null
-> 所有 root 都决策完
-> flushSyncWorkAcrossRoots_impl
-> performSyncWorkOnRoot所以 SyncLane 通常不作为普通 scheduler task 排队,而是在 root schedule microtask 末尾被同步 flush。
3.3 scheduleTaskForRootDuringMicrotask 的分流
scheduleTaskForRootDuringMicrotask(root, currentTime) 是 Root Scheduler 的核心决策点:
function scheduleTaskForRootDuringMicrotask(root, currentTime) {
markStarvedLanesAsExpired(root, currentTime);
const nextLanes = getNextLanes(root, root === workInProgressRoot);
if (nextLanes === NoLanes) {
cancelCallback(root.callbackNode);
root.callbackNode = null;
root.callbackPriority = NoLane;
return NoLane;
}
const newCallbackPriority = getHighestPriorityLane(nextLanes);
const existingCallbackPriority = root.callbackPriority;
if (newCallbackPriority === existingCallbackPriority) {
return newCallbackPriority;
}
cancelCallback(root.callbackNode);
if (includesSyncLane(nextLanes)) {
root.callbackPriority = SyncLane;
root.callbackNode = null;
return SyncLane;
}
const schedulerPriority = lanesToSchedulerPriority(nextLanes);
const callbackNode = scheduleCallback(
schedulerPriority,
performWorkOnRootViaSchedulerTask.bind(null, root)
);
root.callbackPriority = newCallbackPriority;
root.callbackNode = callbackNode;
return newCallbackPriority;
}可以把分流记成:
SyncLane:
不走普通 scheduler package task
-> root.callbackPriority = SyncLane
-> root.callbackNode = null
-> microtask 末尾 flushSyncWorkAcrossRoots_impl
Default / Transition / Idle:
-> lanesToEventPriority(nextLanes)
-> SchedulerPriority
-> scheduleCallback(priority, performWorkOnRootViaSchedulerTask)microtask 和 MessageChannel 的边界图:
sequenceDiagram
participant Event as 浏览器事件 task
participant Micro as microtask
participant Root as Root Scheduler
participant Sch as scheduler package
participant Host as MessageChannel task
Event->>Root: setState / markRootUpdated
Root->>Micro: scheduleMicrotask(processRootScheduleInMicrotask)
Micro->>Root: 遍历 root schedule
Root->>Root: getNextLanes / 分流
alt SyncLane
Root->>Root: flushSyncWorkAcrossRoots_impl
else Concurrent lanes
Root->>Sch: scheduleCallback(priority, callback)
Sch->>Host: port.postMessage(null)
Host->>Sch: performWorkUntilDeadline
end3.4 didScheduleMicrotask 与 mightHavePendingSyncWork
这两个全局布尔值都是为了少做无用功:
字段 | 作用 |
|---|---|
| 防止同一轮里重复安排 root schedule microtask |
| 快速判断是否可能有同步工作;如果为 false,sync flush 可以快速退出 |
mightHavePendingSyncWork 里的 might 表示保守标记。只要 root 收到更新,ensureRootIsScheduled 会先设为 true。等 microtask 真正遍历 roots 时,React 再重新判断是否真的存在需要同步 flush 的 lanes。
到这里,Root Scheduler 已经能把同步更新和并发更新分开。下一步要看:它如何决定“下一批 lanes”到底是谁。
四、Lane 选择:getNextLanes、优先级映射与防止饿死
Lane 是 React reconciler 的更新优先级模型,本质是 bitmask。它既表达优先级,也表达一批更新,还参与 update queue 的跳过与重放、Suspense 状态和 render 模式。
4.1 getNextLanes 不只是取最高优先级
getNextLanes 会综合多种 root 状态:
输入 | 含义 |
|---|---|
| root 上还有哪些待处理工作 |
| 哪些 lanes 当前被 Suspense 等阻塞 |
| 哪些 suspended lanes 已经被数据 resolve 唤醒 |
| 哪些 lanes 已经过期,不能继续饿着 |
| 当前正在 render 的 lanes |
| 是否有等待提交的 work |
一个重要规则:
如果已经在 render 某批 lanes,
新来的 lanes 没有更高优先级,
React 会尽量继续当前 work-in-progress,
避免丢弃已经完成的 render 进度。另一个容易忽略的点:
DefaultLane 默认不轻易打断正在进行的 TransitionLane。Default 更新和 Transition 更新在用户体验语义上不同,但 React 也会尽量避免无谓丢弃已经做完的 transition render 进度。
4.2 markStarvedLanesAsExpired 防止 lane 饥饿
markStarvedLanesAsExpired(root, currentTime) 的目的,是防止某些 pending lanes 因为总被更高优先级工作插队而长期不执行。
它依赖两个 root 字段:
字段 | 含义 |
|---|---|
| 按 lane index 存每条 pending lane 的过期时间 |
| 已经过期、需要优先处理的 lanes bitmask |
简化版源码:
function markStarvedLanesAsExpired(root, currentTime) {
let lanes = root.pendingLanes;
while (lanes > 0) {
const lane = pickArbitraryLane(lanes);
const index = laneToIndex(lane);
const expirationTime = root.expirationTimes[index];
if (expirationTime === NoTimestamp) {
if (!isSuspended(root, lane) || isPinged(root, lane)) {
root.expirationTimes[index] = computeExpirationTime(lane, currentTime);
}
} else if (expirationTime <= currentTime) {
root.expiredLanes |= lane;
}
lanes &= ~lane;
}
}注意易错点:
markStarvedLanesAsExpired 不会把 TransitionLane 改成 SyncLane。
它只是把原来的 lane 标记进 root.expiredLanes。也就是说,过期的 TransitionLane 仍然是 TransitionLane 这个 bit。React 只是通过 expiredLanes 告诉后续选择逻辑:这条 lane 已经等太久,不能继续被饿着。
这个思路和 scheduler package 的 task expirationTime 类似,但层级不同:
层级 | 过期对象 | 作用 |
|---|---|---|
Root Scheduler / Lane | root 上的 lanes | 防止某些 React 更新长期不被选中 |
| callback task | 防止底层任务长期排队或 yield |
4.3 EventPriority、Lane、SchedulerPriority 的关系
这三个优先级模型属于不同层:
名称 | 所属层 | 表达什么 | 典型值 |
|---|---|---|---|
EventPriority | React 事件/更新入口 | 当前事件产生更新的紧急程度 |
|
Lane | React reconciler | 更新优先级集合和 render 语义 |
|
SchedulerPriority |
| callback 任务优先级和 timeout |
|
普通非 transition 更新通常走:
requestUpdateLane
-> resolveUpdatePriority()
-> eventPriorityToLane(updatePriority)常见入口映射:
EventPriority | 常见 Lane |
|---|---|
|
|
|
|
|
|
|
|
如果当前处于 startTransition 上下文,则会走:
requestUpdateLane
-> requestCurrentTransition()
-> requestTransitionLane(transition)
-> TransitionLanerequestTransitionLane 有一个重要规则:
同一个事件里的多个 transition update 会复用同一个 currentEventTransitionLane。Root Scheduler 选择出 nextLanes 后,再把 lanes 映射到底层 scheduler priority:
nextLanes
-> lanesToEventPriority(nextLanes)
-> SchedulerPriority
-> scheduleCallback(priority, callback)常见映射:
lanesToEventPriority 结果 | SchedulerPriority |
|---|---|
| 通常偏 |
|
|
|
|
|
|
最容易踩坑的是:
SchedulerPriority 不等于 Lane。
DefaultLane 和 TransitionLane 都可能映射到 NormalPriority,
但它们在 React reconciler 里的 render 语义并不一样。是否可以时间切片,不能只看 NormalPriority,还要看 lane 是否属于 transition、是否 blocking、是否过期,以及 performWorkOnRoot 最终选择的 render 路径。
4.4 SyncLane 与 TransitionLane 的调度差异
对比项 | SyncLane | TransitionLane |
|---|---|---|
常见来源 | 点击、键盘等离散事件里的普通更新, |
|
用户感知 | 立刻反馈 | 非紧急 UI 变化,可以稍后完成 |
Root Scheduler | microtask 末尾同步 flush | 通常安排 scheduler callback |
| 通常为 | scheduler 返回的 task node |
render 方式 | 通常 | 通常 |
时间切片 | 否 | 未过期时可 yield |
是否可被打断 | 基本不让步 | 可被更高优先级更新打断 |
饥饿处理 | 本身最高优先级 | 过久会被标记为 expired |
面试表达:
SyncLane 用来保证紧急交互的即时反馈,React 会在 microtask 末尾同步 flush;
TransitionLane 表示非紧急更新,Root Scheduler 通常把它映射成 NormalPriority task,
render 阶段可以通过 shouldYield 让出主线程。理解了 lane 的选择和映射,才适合进入第二层:底层 scheduler package 如何执行 callback。
五、第二层模型:scheduler package 管 callback task
scheduler package 是更底层的任务调度工具。它不理解 Fiber、Hook、DOM、lane 或 root。对它来说,React 传入的只是一个 callback:
unstable_scheduleCallback(priorityLevel, callback, options?)它只关心:
callback 的
priorityLevel。callback 是否有 delay,也就是
startTime是否在未来。callback 的
expirationTime。当前任务是否已经过期。
当前时间片是否应该让出主线程。
callback 是否返回 continuation。
可以把它想成一个很小的运行时:
React reconciler:
"我有一段 NormalPriority 的工作,帮我尽快安排。"
scheduler package:
"我给它算 startTime / expirationTime,放进堆里。
到时间后调用它。
如果时间片用完且任务没过期,我先停。
如果它返回 continuation,下轮继续。"5.1 Task 数据结构
Scheduler.js 里的 task 可以抓住这些字段:
字段 | 含义 | 面试理解 |
|---|---|---|
| 自增任务 id | 用于稳定排序和调试 |
| 真正要执行的函数 | React 传入的工作入口,可能返回 continuation |
| scheduler 层优先级 | Immediate、UserBlocking、Normal、Low、Idle |
| 最早可执行时间 | 没有 delay 时通常等于当前时间,有 delay 时是未来时间 |
| 过期时间 |
|
| 堆排序字段 | 在 |
简化对象:
const task = {
id,
callback,
priorityLevel,
startTime,
expirationTime,
sortIndex,
};startTime 和 expirationTime 的关系:
startTime = currentTime + delay
expirationTime = startTime + timeouttimeout 来自 priority。优先级越高,timeout 越短,越容易先过期;优先级越低,timeout 越长,更愿意让给重要任务。
常见 scheduler priorities:
优先级 | 含义 | timeout 特征 |
|---|---|---|
| 必须立刻处理 | timeout 通常为 |
| 会阻塞用户感知的交互 | timeout 较短 |
| 默认普通工作 | 默认 timeout |
| 低优先级工作 | timeout 较长 |
| 空闲工作 | 基本不超时 |
5.2 taskQueue 与 timerQueue
scheduler package 用两个小顶堆管理任务:
队列 | 排序字段 | 存什么 | 作用 |
|---|---|---|---|
|
| 已经到 | 从可执行任务里挑最早过期的先跑 |
|
| 设置了 delay、还没到开始时间的任务 | 找出最早应该被激活的延迟任务 |
为什么需要两个队列?因为“还没到开始时间”和“已经可以执行”的排序目标不同:
timerQueue:
谁的 startTime 最早,谁最先被激活
taskQueue:
谁的 expirationTime 最早,谁最应该执行小顶堆的三个核心操作:
操作 | 含义 |
|---|---|
| 查看堆顶但不移除 |
| 插入任务并维护堆序 |
| 移除并返回堆顶 |
简化版 scheduleCallback:
function scheduleCallback(priorityLevel, callback, options) {
const currentTime = getCurrentTime();
const delay = options?.delay ?? 0;
const startTime = currentTime + delay;
const timeout = timeoutForPriority(priorityLevel);
const expirationTime = startTime + timeout;
const task = {
id: taskIdCounter++,
callback,
priorityLevel,
startTime,
expirationTime,
sortIndex: -1,
};
if (startTime > currentTime) {
task.sortIndex = startTime;
push(timerQueue, task);
requestHostTimeout(handleTimeout, startTime - currentTime);
} else {
task.sortIndex = expirationTime;
push(taskQueue, task);
requestHostCallback();
}
return task;
}这说明 scheduler 不是 FIFO。它真正排序的是“可执行任务里谁更早过期”,同时用 delay 支持未来才开始的任务。
5.3 advanceTimers:延迟任务如何变成可执行任务
advanceTimers(currentTime) 会把已经到开始时间的 delay task 从 timerQueue 移到 taskQueue:
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);
}
}同一个 task 在不同队列中使用不同 sortIndex:
timerQueue 中 sortIndex = startTime
taskQueue 中 sortIndex = expirationTime这就是 startTime、currentTime、expirationTime 三者在 scheduler package 里的核心意义。
scheduler package 有了 task 队列后,还需要浏览器在某个时机调用它。这个时机就是 host callback,浏览器环境下通常由 MessageChannel 驱动。
六、执行链路:MessageChannel、performWorkUntilDeadline 与 workLoop
scheduler package 维护自己的队列,但 JS 不会自己醒来。它需要向宿主环境投递一个 host callback,让浏览器在下一轮任务中调用它。
6.1 MessageChannel 不是 microtask
浏览器环境下,常见链路是:
schedulePerformWorkUntilDeadline()
-> port.postMessage(null)
-> channel.port1.onmessage
-> performWorkUntilDeadline()常见 host 机制:
机制 | 常见环境 | 特点 |
|---|---|---|
| 某些非浏览器或特定运行时 | 比 |
| 浏览器 | 比连续 |
| fallback | 兼容兜底,但可能受 clamping 影响 |
注意:
MessageChannel 不是 microtask。
Root Scheduler 用 microtask 处理 root schedule;
scheduler package 用 MessageChannel 这类 task 驱动分片 work。6.2 performWorkUntilDeadline 到 shouldYieldToHost
底层执行链路:
unstable_scheduleCallback(priority, callback)
-> task 入 taskQueue / timerQueue
-> requestHostCallback()
-> schedulePerformWorkUntilDeadline()
-> performWorkUntilDeadline()
-> flushWork(initialTime)
-> workLoop(initialTime)
-> callback(didUserCallbackTimeout)
-> shouldYieldToHost()关键函数职责:
函数 | 作用 |
|---|---|
| host callback 入口,记录本轮开始时间,调用 |
| 设置执行状态,调用 |
| 从 |
| 判断本轮执行时间是否超过时间片,是否该把主线程还给浏览器 |
简化版源码:
function performWorkUntilDeadline() {
if (scheduledHostCallback !== null) {
const currentTime = getCurrentTime();
startTime = currentTime;
const hasMoreWork = scheduledHostCallback(currentTime);
if (hasMoreWork) {
schedulePerformWorkUntilDeadline();
} else {
isMessageLoopRunning = false;
scheduledHostCallback = null;
}
}
}function flushWork(initialTime) {
isHostCallbackScheduled = false;
isPerformingWork = true;
try {
return workLoop(initialTime);
} finally {
currentTask = null;
isPerformingWork = false;
}
}6.3 workLoop 消费 taskQueue
这里的 workLoop 是 scheduler package 的 work loop,不是 ReactFiberWorkLoop 里的 Fiber work loop。它管的是 callback task,不是 Fiber 节点。
简化版源码:
function workLoop(initialTime) {
let currentTime = initialTime;
advanceTimers(currentTime);
currentTask = peek(taskQueue);
while (currentTask !== null) {
const taskHasNotExpired = currentTask.expirationTime > currentTime;
if (taskHasNotExpired && shouldYieldToHost()) {
break;
}
const callback = currentTask.callback;
if (typeof callback === 'function') {
currentTask.callback = null;
const didTimeout = currentTask.expirationTime <= currentTime;
const continuation = callback(didTimeout);
currentTime = getCurrentTime();
if (typeof continuation === 'function') {
currentTask.callback = continuation;
advanceTimers(currentTime);
return true;
}
if (currentTask === peek(taskQueue)) {
pop(taskQueue);
}
} else {
pop(taskQueue);
}
advanceTimers(currentTime);
currentTask = peek(taskQueue);
}
return currentTask !== null;
}逐步理解:
advanceTimers(currentTime):到期 timer 进入taskQueue。peek(taskQueue):取当前最应该执行的 task。如果任务未过期且
shouldYieldToHost()为 true,就 break,让出主线程。执行
callback(didTimeout)。如果 callback 返回函数,把它保存成 continuation。
如果 callback 不返回函数,说明任务完成,从
taskQueuepop。workLoop返回 true,performWorkUntilDeadline会投递下一轮 MessageChannel task。
流程图:
6.4 shouldYieldToHost 与时间切片
时间切片不是固定每 16.6ms 执行一次。scheduler 会记录一轮 host callback 的开始时间,再用类似 frameYieldMs 的阈值判断这轮 JS 已经跑了多久:
function shouldYieldToHost() {
const timeElapsed = getCurrentTime() - startTime;
return timeElapsed >= frameYieldMs;
}简化理解:
startTime = 本轮 host callback 开始时间
currentTime = 当前时间
timeElapsed = currentTime - startTime
timeElapsed < frameYieldMs:
继续执行
timeElapsed >= frameYieldMs:
shouldYieldToHost() 返回 true,让出主线程但过期任务是例外:
如果 task 未过期,并且 shouldYieldToHost() 为 true
-> 让出主线程
如果 task 已过期
-> 倾向继续执行,避免任务长期饥饿workLoop 时间切片示意:
MessageChannel task #1
|---------------- host callback ----------------|
start
task callback -> Fiber A -> Fiber B -> Fiber C
shouldYield = true
return continuation
浏览器有机会处理输入 / 绘制
MessageChannel task #2
|---------------- host callback ----------------|
continuation -> Fiber D -> Fiber E -> ...到这里,底层 scheduler 只是在执行 callback。React render 是否能恢复,要看 continuation 和 Fiber WIP 状态如何配合。
七、React 连接点:performWorkOnRoot、continuation 与 render/commit 边界
Root Scheduler 投递到底层 scheduler 的 callback,通常会进入 performWorkOnRootViaSchedulerTask,再进入 React 自己的 work loop。
7.1 scheduler callback 如何进入 React render
并发更新的大致链路:
scheduleCallback(priority, performWorkOnRootViaSchedulerTask)
-> scheduler workLoop 执行 callback(didTimeout)
-> performWorkOnRootViaSchedulerTask(root, didTimeout)
-> performWorkOnRoot(root, lanes, forceSync = false)
-> renderRootConcurrent(root, lanes)
-> workLoopConcurrentByScheduler()React concurrent render 的关键循环:
function workLoopConcurrentByScheduler() {
while (workInProgress !== null && !shouldYield()) {
performUnitOfWork(workInProgress);
}
}shouldYield() 最终来自 scheduler package 的 shouldYieldToHost()。也就是说,底层 scheduler 负责判断“这一轮 host callback 是否该让出主线程”,React Fiber work loop 负责在 Fiber 单元之间尊重这个判断。
7.2 continuation callback 表示任务未完成
Scheduler callback 的返回值很关键:
const continuation = callback(didUserCallbackTimeout);
if (typeof continuation === 'function') {
currentTask.callback = continuation;
return true;
}如果 callback 返回函数,scheduler 会认为这个 task 没完成,把返回函数保存为 continuation,下一个 host callback 再调用。
容易误解的点:
continuation 不是从上一个 JS 函数体的中间一行恢复。
普通 JS 函数不会自动保存调用栈。
scheduler 只是保存 callback 返回的新函数,下轮从这个函数开头再调用。React 看起来像“接着渲染”,是因为 render 进度保存在 Fiber 层:
状态 | 作用 |
|---|---|
| 当前处理到的 Fiber |
| 当前正在 render 的 root |
| 当前 render 的 lanes |
WIP Fiber 树 | 已经计算出的中间结果 |
时间片例子:
时间片 1:
performWorkOnRootViaSchedulerTask()
-> renderRootConcurrent()
-> 处理 Fiber A / B / C
-> shouldYield() 为 true
-> workInProgress 指向 Fiber D
-> 返回 continuation
时间片 2:
scheduler 调用 continuation()
-> 重新进入 performWorkOnRoot
-> 如果 root / lanes 仍可复用
-> 从 Fiber D 附近继续如果来了更高优先级更新、root/lanes 变化、Suspense retry、错误恢复等,React 也可能丢弃当前 WIP 并重新开始。这不是 continuation 本身决定的,而是 Root Scheduler 和 Fiber 调度策略决定的。
7.3 render 可中断,commit 不可中断
render 阶段做的是内存计算:
从 current 树创建 work-in-progress 树。
执行
beginWork。计算新 props / state。
reconcile children。
执行
completeWork。收集 flags。
这些工作还没有真正修改 DOM。所以如果时间片到了或者来了更高优先级更新,React 可以暂停当前 work loop,后续继续,或者丢弃 WIP 重新开始。
commit 阶段不同,它会真正修改宿主环境:
before mutation effects。
mutation effects:插入、删除、更新 DOM。
root.current = finishedWork。layout effects。
如果 commit 可以中断,页面可能出现 DOM 更新了一半、root.current 切换不一致、layout effect 读到半更新 DOM 等问题。因此 commit 必须原子完成,不做时间切片。
面试表达:
render 阶段是内存中的可丢弃计算,可以中断;
commit 阶段会产生真实副作用,包括 DOM mutation、ref、layout effect 和 current 树切换,
必须一次性完成。补充:useEffect 对应的 passive effects 可以在 commit 后异步 flush,但这不改变 mutation/layout commit 不可中断的原则。
7.4 一次点击 setState 与一次 transition 的对比
普通点击 setState:
flowchart TD
A["click"] --> B["DiscreteEventPriority"]
B --> C["requestUpdateLane"]
C --> D["SyncLane"]
D --> E["markRootUpdated"]
E --> F["ensureRootIsScheduled"]
F --> G["microtask: processRootScheduleInMicrotask"]
G --> H["getNextLanes 选中 SyncLane"]
H --> I["callbackPriority = SyncLane; callbackNode = null"]
I --> J["flushSyncWorkAcrossRoots_impl"]
J --> K["performSyncWorkOnRoot"]
K --> L["renderRootSync"]
L --> M["commitRoot"]startTransition 内的更新:
flowchart TD
A["startTransition"] --> B["requestTransitionLane"]
B --> C["TransitionLane"]
C --> D["markRootUpdated"]
D --> E["microtask: processRootScheduleInMicrotask"]
E --> F["getNextLanes 选中 TransitionLane"]
F --> G["lanesToEventPriority -> DefaultEventPriority"]
G --> H["scheduleCallback NormalPriority"]
H --> I["MessageChannel task"]
I --> J["performWorkOnRootViaSchedulerTask"]
J --> K["renderRootConcurrent"]
K --> L{"shouldYield?"}
L -- "是" --> M["返回 continuation"]
M --> I
L -- "否" --> N["render 完成"]
N --> O["commitRoot 原子提交"]核心区别:
SyncLane 追求立即反馈,通常 microtask 末尾同步 render + commit;
TransitionLane 追求不阻塞交互,通常进入 concurrent render,可 yield,可被更高优先级打断。八、面试问答与易错点
Q1: React 为什么需要 Scheduler?
React render 可能很重,如果一次同步处理完整棵 Fiber 树,会长时间占用浏览器主线程,阻塞点击、输入、滚动和绘制。Scheduler 的作用是在主线程上做协作式调度:紧急更新优先处理,非紧急 render 可以切片、让步、恢复或重来,从而提升响应性。它不是多线程,也不是浏览器强制抢占 JS。
Q2: Root Scheduler 和 scheduler package 是一回事吗?
不是。Root Scheduler 在 react-reconciler 内部,理解 root、fiber、lanes、pending/suspended/pinged/expired 状态,负责选 root、选 lanes、决定同步 flush 还是并发 callback。scheduler package 是底层任务调度工具,只管理 callback task、priority、startTime、expirationTime、MessageChannel、shouldYieldToHost 和 continuation,不理解 React 语义。
Q3: 同一事件内多次 setState 是怎么合并调度的?
每次 setState 都会创建 update 并入队,然后通过 markRootUpdated 把 lane OR 到 root.pendingLanes。但同一个 root 不会重复加入 root schedule 链表,didScheduleMicrotask 也避免重复安排 microtask。因此 React 会在当前事件结束后的同一个 microtask 中统一遍历 roots 和选择 lanes。合并发生在 root/lane 调度层,不是丢弃 update。
Q4: microtask 在 Root Scheduler 里做什么?
microtask 用来在当前事件回调结束后尽快处理 root schedule。它会进入 processRootScheduleInMicrotask,遍历 firstScheduledRoot -> root.next 链表,对每个 root 调用 scheduleTaskForRootDuringMicrotask。最后统一调用 flushSyncWorkAcrossRoots_impl 处理同步工作。
Lane 是 React 内部的更新优先级集合,用 bitmask 表示,参与 update queue、root pending lanes、Suspense 状态、render 路径选择和可中断语义。SchedulerPriority 是 scheduler package 的 callback 任务优先级,用于计算 timeout 和排序 task。React 会把 lanes 经 lanesToEventPriority 映射成 SchedulerPriority,但二者不是一一对应。
Q5: continuation callback 是从上次函数中间继续吗?
不是。普通 JS 函数不会自动恢复调用栈。scheduler 只是保存 callback 返回的新函数,下轮 host callback 从这个函数开头再调用。React render 能“接着做”,是因为 Fiber 进度保存在 workInProgress、workInProgressRoot、WIP Fiber 树和 render lanes 中。
Q6: render 为什么可以中断,commit 为什么不能中断?
render 阶段是内存中的计算,主要构建 work-in-progress Fiber 树、计算 state/children、收集 flags,还没有修改 DOM,因此可以暂停、恢复或丢弃重来。commit 阶段会真实修改 DOM、处理 ref、layout effect 并切换 root.current,必须保持页面和 Fiber current 树一致,所以不能中断。


评论
还没有评论
来发表第一条评论吧!