
Contents
点击按钮 → setState(= dispatchSetState)
│
├─ ① 建更新、入队
│ requestEventTime() 取「当前事件时间」
│ requestUpdateLane(fiber) 选一个「车道(优先级)」
│ createUpdate(eventTime, lane)
│ enqueueUpdate(fiber, update) 把 update 挂到 fiber 的 updateQueue
│
└─ ② 调度
scheduleUpdateOnFiber(root, fiber, lane, eventTime)
├─ checkIfNestedUpdatePriority(...) 嵌套更新检测(render 中更新?)
├─ markRootUpdated(root, lane, eventTime) 记录「根上有待处理车道」
└─ ensureRootIsScheduled(root, eventTime)
├─ getNextLanes(root) 算出「这一轮要处理哪些车道」
├─ 是 SyncLane?
│ └─ scheduleMicrotask(flushSyncCallbacks) → 微任务里同步渲染
└─ 否
├─ lanesToEventPriority(lanes) 车道 → Scheduler 优先级
└─ scheduleCallback(priority, performConcurrentWorkOnRoot.bind(null, root))
└─ Scheduler(scheduler 包)
push(taskQueue) → requestHostCallback(flushWork)
→ MessageChannel.port2.postMessage(null)
→ performWorkUntilDeadline
while (!shouldYieldToHost()) { task.callback() } ⭐ 时间片
■ 渲染阶段(可中断)
performConcurrentWorkOnRoot(root, didTimeout)
├─ getNextLanes / shouldTimeSlice 判断
├─ renderRootConcurrent(root, lanes)
│ ├─ prepareFreshStack(root, lanes) 建 workInProgress
│ └─ workLoopConcurrent()
│ while (workInProgress !== null && !shouldYield()) {
│ performUnitOfWork(workInProgress)
│ }
│ ├─ beginWork(...) → 返回 child(「递」)
│ └─ completeUnitOfWork → completeWork(「归」)+ sibling/return
└─ 若本帧没跑完 → 返回「继续函数」,让 Scheduler 再调度一次
■ 提交阶段(不可中断)
commitRoot(root) → commitRootImpl(root, ...)
├─ flushPassiveEffects() 先清上一次的 useEffect
├─ commitBeforeMutationEffects() getSnapshotBeforeUpdate / 调度 passive
├─ commitMutationEffects() ⭐ 改 DOM(内部先跑 useInsertionEffect)
├─ root.current = finishedWork ⭐ 双缓存切换
├─ commitLayoutEffects() ⭐ 赋 ref + 跑 useLayoutEffect
└─ 调度 passive effects(微任务里跑 useEffect)
updateContainer —— 建立并登记一次更新// ✅ React 源码结构(简化,保留关键步骤与命名)
function updateContainer(element, container, parentComponent, callback) {
const current = container.current; // HostRootFiber
const eventTime = requestEventTime(); // 「现在」
const lane = requestUpdateLane(current); // ⭐ 选车道(优先级)
// ① 建「更新对象」
const update = createUpdate(eventTime, lane);
update.payload = { element }; // render(x) 的 x
if (callback) update.callback = callback;
// ② 入队:挂到 fiber.updateQueue(环形链表)
enqueueUpdate(current, update, lane);
// ③ 调度
const root = scheduleUpdateOnFiber(current, lane, eventTime);
return lane;
}
⭐⭐ 三个关键点(每一步都值得理解):
① 【`requestEventTime()` —— 为什么要「事件时间」?】
它返回「触发这次更新的时间点」(同一批事件里的更新会共享同一个 eventTime)。
用途:计算「过期时间」——低优先级更新「等太久」时会「过期」,
过期后会被【强制同步执行】(避免无限期饿死)。
→ 这就解释了「为什么低优先级任务最终一定会执行」:因为它们会过期。
② 【`requestUpdateLane(fiber)` —— 谁决定优先级?】
它根据「当前上下文」选车道:
· 在 React 事件处理里(离散事件)→ SyncLane / InputContinuousLane
· 在 startTransition 里 → TransitionLanes
· 其它 → DefaultLane
→ 这是「点击里的 setState 表现得像同步」的**真正原因**(不是「React 特殊照顾点击」,
而是「点击被分配了 SyncLane」)。
③ 【`enqueueUpdate` —— 更新存在哪里?】
存在 `fiber.updateQueue` 上(一个「环形链表」,带 `shared.pending` 指针)。
用环形链表是为了「O(1) 追加」+「方便在 render 时一次性切断消费」。
scheduleUpdateOnFiber 与 ensureRootIsScheduledfunction scheduleUpdateOnFiber(root, fiber, lane, eventTime) {
// ① 检查「是否在 render 中触发了更新」(嵌套更新)→ 用于「同步冲洗」避免撕裂
checkIfNestedUpdatePriority(root, fiber);
// ② 在「根」上标记:有新的车道需要处理
markRootUpdated(root, lane, eventTime);
// ③ 确保「已经安排了一次调度」(去重:同一批更新只调度一次)
ensureRootIsScheduled(root, eventTime);
}
function ensureRootIsScheduled(root, currentTime) {
// ① 取出「根上所有待处理车道」中「这一轮要处理的那些」
const nextLanes = getNextLanes(root, root.pendingLanes);
if (nextLanes === NoLanes) return; // 没活干了
// ② 取「最高优先级的那一条车道」
const newCallbackPriority = getHighestPriorityLane(nextLanes);
// ③ 如果「已经有一个同样优先级的调度在进行」→ 直接复用,不重复调度
const existingCallbackPriority = root.callbackPriority;
if (existingCallbackPriority === newCallbackPriority) return;
// ④ 按优先级分两条路
if (newCallbackPriority === SyncLane) {
// ⭐ 同步车道:用微任务(而不是「立刻执行」)
scheduleMicrotask(flushSyncCallbacks);
} else {
// ⭐ 其它车道:交给 Scheduler(可中断)
const schedulerPriorityLevel = lanesToEventPriority(nextLanes);
scheduleCallback(
schedulerPriorityLevel,
performConcurrentWorkOnRoot.bind(null, root)
);
}
}
⭐⭐⭐ 三个「为什么」:
① 【为什么 SyncLane 用 `scheduleMicrotask` 而不是直接调用渲染?】
因为「同步渲染」发生在「事件处理函数返回之后」——
如果直接调用,就会出现「setState 立刻渲染」的语义,
那样「一次点击里的多次 setState」就不再批处理了。
用微任务 → 等当前同步代码跑完 → 再统一渲染 → **这就是批处理的实现方式**。
② 【为什么用 `lanesToEventPriority` 做「车道 → Scheduler 优先级」的映射?】
因为「车道」是 React 自己的优先级体系(用于「能否合并」「能否被打断」),
而「Scheduler 优先级」是给调度器用的(决定「用不用时间片、让不让路」)。
两套体系需要翻译:Sync 车道 → 同步路径;Transition 车道 → Low/Idle 优先级
(所以「过渡更新」在 Scheduler 眼里是「低优先级」→ 更容易被让路)。
③ 【为什么要「去重」(`existingCallbackPriority` 判断)?】
同一个优先级已经有调度在排队时,再排一次没意义(会跑两遍)。
→ 这也是「一个事件循环里 100 次 setState 只渲染一次」的机制之一。
// ✅ scheduler 包的核心(简化,保留关键命名)
const frameYieldMs = 5; // ⭐ 时间片长度(约 5ms)
let taskQueue = []; // 按 expirationTime 排序的小顶堆
let currentTask = null;
let taskIdCounter = 1;
let startTime = -1;
let currentTime = -1;
let deadline = 0;
function scheduleCallback(priorityLevel, callback, options) {
const currentTime = getCurrentTime();
const startTime = currentTime;
const timeout = timeoutByPriority[priorityLevel]; // 不同优先级不同超时
const expirationTime = startTime + timeout;
const newTask = {
id: taskIdCounter++,
callback,
priorityLevel,
startTime,
expirationTime,
sortIndex: -1,
};
push(taskQueue, newTask); // 入堆
requestHostCallback(flushWork); // ⭐ 请求「宿主」在合适时机执行
return newTask;
}
// ⭐⭐ 关键的「宿主调度」:用 MessageChannel 而不是 requestIdleCallback
const channel = new MessageChannel();
const port = channel.port2;
function requestHostCallback(callback) {
scheduledHostCallback = callback;
if (!isMessageLoopRunning) {
isMessageLoopRunning = true;
schedulePerformWorkUntilDeadline(); // 默认实现:port.postMessage(null)
}
}
channel.port1.onmessage = performWorkUntilDeadline;
function performWorkUntilDeadline() {
const currentTime = getCurrentTime();
// ⭐ 设置「这一帧的时间片 deadline」
startTime = currentTime;
deadline = currentTime + frameYieldMs; // ≈ 5ms 之后必须让出
const hasMoreWork = scheduledHostCallback?.(true);
if (hasMoreWork) {
schedulePerformWorkUntilDeadline(); // 还有活 → 再排一个消息,下一轮继续
} else {
isMessageLoopRunning = false;
}
}
function shouldYieldToHost() {
return getCurrentTime() >= deadline; // ⭐ 时间片用完 → 让出主线程
}
⭐⭐⭐ 为什么用 `MessageChannel` 而不用 `requestIdleCallback` / `requestAnimationFrame`?
① `requestIdleCallback`:
· 兼容性差(Safari 长期不支持)
· 调用频率被限制(相邻回调之间的间隔可能很长,规范允许「最多 ~20 次/秒」)
· 「空闲时间」由浏览器决定,**不可控**——React 需要「稳定的 5ms 时间片」
→ 所以它只适合「真的不急」的任务,不适合 React 的调度
② `requestAnimationFrame`:
· 只在「页面可见」时才触发(**后台标签页会暂停** → 更新被挂起)
· 一帧只调一次(粒度太粗)
→ 适合动画,不适合「需要多次让出」的调度
③ `setTimeout`:
· 有「最小延迟」限制(嵌套 5 层后至少 4ms)
· 精度差、且在后台标签页被节流到 1s
→ 达不到「5ms 时间片」的精度要求
④ `MessageChannel`:
· 是「宏任务」,但**不需要等待固定延迟**(比 setTimeout 更及时)
· 在所有主流浏览器可用(包括 Worker 里)
· 可以「一次任务里多次 postMessage」来「连续让出」
→ ⭐ 因此成为 Scheduler 的默认实现
⭐ 一个「传得很广但不准确」的说法要澄清:
❌ 「React 用 requestIdleCallback 做时间切片」
✅ React 用的是「MessageChannel + 自己的 5ms 时间片」;
`requestIdleCallback` 只在「Scheduler 的某个降级路径」里出现(且不是默认)。
function performConcurrentWorkOnRoot(root, didTimeout) {
const lanes = getNextLanes(root, root.pendingLanes);
if (lanes === NoLanes) return null;
// ⭐ 判断「是否可以用时间切片(并发渲染)」
const shouldTimeSlice =
!includesBlockingLane(lanes) && // 不含「阻塞车道」(Sync / InputContinuous)
!includesExpiredLane(root, lanes) && // 没有「已过期」的车道(过期必须同步做完)
!didTimeout; // Scheduler 没告诉我们「超时了」
const exitStatus = shouldTimeSlice
? renderRootConcurrent(root, lanes)
: renderRootSync(root, lanes);
if (exitStatus !== RootInProgress) {
// 渲染完成 → 提交
commitRoot(root, /* ... */);
}
// ⭐ 返回「继续函数」:如果本帧没跑完,Scheduler 会用这个函数再调度一次
return root.callbackNode === originalCallbackNode
? performConcurrentWorkOnRoot.bind(null, root)
: null;
}
function renderRootConcurrent(root, lanes) {
prepareFreshStack(root, lanes); // 建 workInProgress 树(双缓存)
workLoopConcurrent();
// ...
}
function workLoopConcurrent() {
// ⭐⭐ 可中断的关键:每处理一个单元就检查「是否该让出」
while (workInProgress !== null && !shouldYield()) {
performUnitOfWork(workInProgress);
}
// 循环退出时:
// · workInProgress === null → 整棵树处理完了
// · shouldYield() === true → 让出主线程(下次从这里继续)
}
function performUnitOfWork(unitOfWork) {
const current = unitOfWork.alternate;
const next = beginWork(current, unitOfWork, renderLanes); // ⭐「递」
unitOfWork.memoizedProps = unitOfWork.pendingProps;
if (next === null) {
completeUnitOfWork(unitOfWork); // ⭐ 没有子节点 → 「归」
} else {
workInProgress = next; // 进入子节点
}
}
function completeUnitOfWork(unitOfWork) {
let completedWork = unitOfWork;
do {
completeWork(current, completedWork, renderLanes); // 处理「完成」逻辑
// 收集副作用(flags)到父节点(subtreeFlags)
// ...
const siblingFiber = completedWork.sibling;
if (siblingFiber !== null) {
workInProgress = siblingFiber; // 处理兄弟
return;
}
completedWork = completedWork.return; // 回到父节点
workInProgress = completedWork;
} while (completedWork !== null);
// 回到根 → 渲染阶段结束
}
⭐⭐ 「可中断」的两个前提(结合 4.2 理解):
① 【遍历是「链表 + 循环」而不是递归】
`workInProgress` 只是一个「全局指针」——所以可以随时「保存并返回」。
递归的调用栈无法保存,这是 Fiber 用链表的核心原因。
② 【「是否让出」是「每个工作单元检查一次」】
`while (workInProgress !== null && !shouldYield())`
→ 粒度是「一个组件/一个节点」的渲染,通常远小于 5ms。
⚠️ 注意:**commit 阶段不在这个循环里**——它由 `commitRoot` 单独完成,不可中断。
commitRootImpl —— 不可中断的提交function commitRootImpl(root, recoverableErrors, transitions) {
// ① 先把「上一次的 passive effects」清掉(避免顺序错乱)
do {
flushPassiveEffects();
} while (rootWithPendingPassiveEffects !== null);
const finishedWork = root.finishedWork;
if (finishedWork === null) return;
root.finishedWork = null;
// ② before mutation:读取 DOM 快照(getSnapshotBeforeUpdate)、调度 passive effects
commitBeforeMutationEffects(root, finishedWork);
// ③ mutation:⭐ 把变更应用到 DOM(内部先跑 useInsertionEffect)
commitMutationEffects(root, finishedWork, lanes);
// ⭐⭐ 双缓存切换:current 指向新的树(在 layout 之前完成)
root.current = finishedWork;
// ④ layout:赋 ref、执行 useLayoutEffect、触发 componentDidMount/DidUpdate
commitLayoutEffects(finishedWork, root, lanes);
// ⑤ 如果本次有 passive effects → 调度它们(异步执行 useEffect)
if (rootDoesHavePassiveEffects) {
rootWithPendingPassiveEffects = root;
}
// ⑥ 冲洗「同步回调队列」(flushSync 与 SyncLane 的任务)
flushSyncCallbacks();
}
| 子阶段 | 做什么 | 典型案例 |
|---|---|---|
| before mutation | 读 DOM 快照 | getSnapshotBeforeUpdate(类组件) |
| mutation | 改 DOM(插入/更新/删除)+ 执行 useInsertionEffect |
<div> 属性更新、节点删除 |
| 双缓存切换 | root.current = finishedWork |
后续的读操作看到「新树」 |
| layout | 赋 ref、执行 useLayoutEffect、componentDidMount/DidUpdate |
测量、焦点、同步副作用 |
| passive(异步) | 执行 useEffect |
请求、订阅、日志 |
⭐⭐ 三个「顺序为什么是这样」的问题(面试常问):
① 【为什么 `useInsertionEffect` 在 mutation 里、而 `useLayoutEffect` 在 layout 里?】
insertion 要在「DOM 变更之前」把样式插进样式表(让浏览器一次算对布局),
layout 要在「DOM 变更之后」读/写布局(此时 DOM 已是新的)。
→ 参见 2.17
② 【为什么 `root.current = finishedWork` 放在 mutation 之后?】
因为在 mutation 阶段,「新树的 DOM」正在被写入真实文档,
此时读取操作(如 `getSnapshotBeforeUpdate`、布局相关逻辑)仍然应该看到「旧树」;
等 DOM 变更完成后再切换 `current`,后续(layout 阶段)的逻辑才看到新树。
→ 这个「切换时机」是「双缓存 + 副作用顺序」的关键衔接点。
③ 【为什么 commit 不能中断?】
因为「DOM 变更做到一半」会让用户看到不一致的界面
(列表删了一半、样式应用了一部分)。
→ React 的取舍是「render 可以慢慢算(可中断),commit 必须一口气做完」。
下面把「时间片 + 让出 + 可中断工作循环」这三件事用 210 行实现出来,可以直接在浏览器里跑并观察「让出主线程」。
// mini-scheduler.ts —— 教学版 Scheduler(复现 MessageChannel + 时间片 + 让出)
type PriorityLevel = 'immediate' | 'userBlocking' | 'normal' | 'low' | 'idle';
const TIMEOUT_BY_PRIORITY: Record<PriorityLevel, number> = {
immediate: -1, // 立即(同步)
userBlocking: 250, // 需要快速响应(如输入)
normal: 5000,
low: 10000,
idle: 1073741823, // 尽量延后
};
interface Task {
id: number;
callback: (didTimeout: boolean) => boolean | void;
priorityLevel: PriorityLevel;
expirationTime: number;
sortIndex: number;
}
let taskQueue: Task[] = [];
let taskIdCounter = 0;
let isMessageLoopRunning = false;
let scheduledHostCallback: ((didTimeout: boolean) => boolean) | null = null;
/** ⭐ 时间片长度(与 React 的 frameYieldMs = 5 一致) */
const FRAME_YIELD_MS = 5;
let startTime = 0;
let deadline = 0;
let currentTime = 0;
const getCurrentTime = () => performance.now();
/** 小顶堆(按 expirationTime 排序)——这里用简化版:插入后排序 */
function push(queue: Task[], task: Task) {
task.sortIndex = task.expirationTime;
queue.push(task);
queue.sort((a, b) => a.sortIndex - b.sortIndex);
}
function peek(queue: Task[]): Task | undefined {
return queue[0];
}
function pop(queue: Task[]): Task | undefined {
const t = queue.shift();
return t;
}
/** ⭐⭐ 宿主调度:用 MessageChannel(与 React 的选择一致) */
const channel = typeof MessageChannel !== 'undefined' ? new MessageChannel() : null;
function schedulePerformWorkUntilDeadline() {
if (channel) {
channel.port2.postMessage(null); // → 触发 port1.onmessage
} else {
setTimeout(performWorkUntilDeadline, 0); // Node/测试环境降级
}
}
if (channel) channel.port1.onmessage = () => performWorkUntilDeadline();
function performWorkUntilDeadline() {
currentTime = getCurrentTime();
startTime = currentTime;
deadline = currentTime + FRAME_YIELD_MS; // ⭐ 设置本帧的时间片
const hasMoreWork = scheduledHostCallback?.(true);
if (hasMoreWork) {
schedulePerformWorkUntilDeadline(); // 还有活 → 再排一次(下一帧继续)
} else {
isMessageLoopRunning = false;
scheduledHostCallback = null;
}
}
/** ⭐ 让出判断:时间片用完了吗 */
export function shouldYieldToHost(): boolean {
return getCurrentTime() >= deadline;
}
/** 对外 API:安排一个任务 */
export function scheduleCallback(
priorityLevel: PriorityLevel,
callback: (didTimeout: boolean) => boolean | void
): Task {
const time = getCurrentTime();
const expirationTime = priorityLevel === 'immediate' ? -1 : time + TIMEOUT_BY_PRIORITY[priorityLevel];
const task: Task = {
id: taskIdCounter++,
callback,
priorityLevel,
expirationTime,
sortIndex: -1,
};
push(taskQueue, task);
requestHostCallback(flushWork);
return task;
}
function requestHostCallback(callback: (didTimeout: boolean) => boolean) {
scheduledHostCallback = callback;
if (!isMessageLoopRunning) {
isMessageLoopRunning = true;
schedulePerformWorkUntilDeadline();
}
}
/** ⭐ 工作循环:在时间片内不断取任务执行,超时就返回「还有活」 */
function flushWork(didTimeout: boolean): boolean {
currentTime = getCurrentTime();
startTime = currentTime;
deadline = currentTime + FRAME_YIELD_MS;
return workLoop(didTimeout);
}
function workLoop(didTimeout: boolean): boolean {
let currentTask = peek(taskQueue);
while (currentTask !== null) {
// ⭐ 如果时间片用完,且任务没超时 → 让出主线程(下次继续)
if (shouldYieldToHost() && currentTask.expirationTime > currentTime) {
break;
}
const callback = currentTask.callback;
const didTimeoutForTask = currentTask.expirationTime <= currentTime;
if (typeof callback === 'function') {
currentTask.callback = () => {}; // 先清掉,避免重入
const continuation = callback(didTimeoutForTask || didTimeout);
if (typeof continuation === 'function') {
// ⭐ 任务「没做完」→ 用返回的「继续函数」替换回调,等下一帧再执行
currentTask.callback = continuation as unknown as (t: boolean) => boolean | void;
} else {
pop(taskQueue); // 任务完成
}
} else {
pop(taskQueue);
}
currentTask = peek(taskQueue);
}
return currentTask !== null; // 还有任务 → 告诉宿主「继续排」
}
// ✅ 用一个「可中断的渲染任务」验证:观察它被切成多片
import { scheduleCallback, shouldYieldToHost } from './mini-scheduler';
/** 模拟「一棵 fiber 树的工作单元」(用数组代替链表,逻辑一样) */
interface Unit {
id: number;
cost: number; // 这个单元的「渲染耗时」(用忙等模拟)
}
function buildUnits(count: number, costEach: number): Unit[] {
return Array.from({ length: count }, (_, i) => ({ id: i, cost: costEach }));
}
function busyWait(ms: number) {
const end = performance.now() + ms;
// 微小的忙等(模拟「渲染一个组件」的同步计算)
while (performance.now() < end) { /* spin */ }
}
/** ⭐ 可中断的「渲染」:每处理一个单元就检查「是否该让出」 */
export function runInterruptibleWork(units: Unit[], onProgress: (done: number) => void) {
let index = 0;
const total = units.length;
const t0 = performance.now();
const workLoop = (): (() => boolean | void) | void => {
while (index < total && !shouldYieldToHost()) {
busyWait(units[index]!.cost); // 模拟渲染这个单元
index++;
}
onProgress(index);
if (index < total) {
console.log(`[mini] 让出主线程(已处理 ${index}/${total},耗时 ${(performance.now() - t0).toFixed(1)}ms)`);
return workLoop; // ⭐ 返回「继续函数」→ 下一帧接着跑
}
console.log(`[mini] 全部完成(总耗时 ${(performance.now() - t0).toFixed(1)}ms,被切成多片)`);
};
scheduleCallback('normal', workLoop);
}
/** ============ 在页面上验证 ============ */
export function SchedulerDemo() {
return (
<button
onClick={() => {
const units = buildUnits(300, 0.3); // 300 个单元 × 0.3ms ≈ 90ms
runInterruptibleWork(units, (done) => {
document.title = `已处理 ${done}`; // 边跑边更新(说明主线程没被完全占用)
});
}}
>
跑可中断渲染(观察控制台的分片日志)
</button>
);
}
⭐⭐ 跑起来你会看到(控制台):
[mini] 让出主线程(已处理 17/300,耗时 5.2ms)
[mini] 让出主线程(已处理 34/300,耗时 10.4ms)
...
[mini] 全部完成(总耗时 92.3ms,被切成多片)
⭐ 三个「与 React 一致」的观察点:
① 每个时间片约 5ms(`FRAME_YIELD_MS`)
② 「让出」是通过「返回一个继续函数」实现的——Scheduler 把它存回任务,
下一帧再调用(真实 React 里也是这个模式:`performConcurrentWorkOnRoot`
返回「自己」表示「还没做完」)
③ 因为「让出」依赖 MessageChannel(宏任务),
期间浏览器有机会处理输入/绘制 —— 所以「长渲染不阻塞交互」
① 认为「React 用 requestIdleCallback 做时间切片」
→ 实际是「MessageChannel + 5ms 时间片」;rIC 只在降级路径出现
② 认为「setState 之后立刻渲染」
→ SyncLane 走「微任务」→ 当前同步代码跑完才渲染(这是批处理的实现)
③ 认为「所有更新都可中断」
→ SyncLane / InputContinuousLane(含 blocking lane)与「已过期」的车道
会走 `renderRootSync`,【不可中断】
④ 认为「commit 也能被打断」
→ `commitRoot` 是独立调用,不在 workLoop 里(不可中断)
⑤ 以为「时间片固定 5ms 一次」
→ 它是「每一帧重新设置 deadline」;且 `flushWork` 每次进入都会重置时间片
⑥ 认为「低优先级任务可能永远不执行」
→ 它们有「过期时间」(`expirationTime`)→ 过期后会被同步完成(防饿死)
⑦ 把「车道(Lane)」与「Scheduler 优先级」混为一谈
→ 两套体系:车道是 React 内部的(能否合并/中断),
Scheduler 优先级是给调度器的(用不用时间片)——中间靠 `lanesToEventPriority` 翻译
⑧ 以为「enqueueUpdate 只是 push 到数组」
→ 是「环形链表 + pending 指针」(为了 O(1) 追加与整批消费)
⑨ 认为「`root.current = finishedWork` 在 commit 最开始」
→ 它在 mutation 之后、layout 之前
⑩ 用 `setTimeout(fn, 0)` 模拟「让出主线程」
→ 嵌套多层后会有 4ms 最小延迟,精度差;后台标签页还会被节流到 1s
⑪ 以为「workInProgress 是一个局部变量」
→ 它是「当前工作单元指针」(可以是模块级/全局的),这正是「可暂停」的前提
⑫ 忽略「render 阶段可能被『重放』」
→ 所以 render 必须纯(否则副作用会重复)——这是 StrictMode 双调用的理论基础
⑬ 以为「Scheduler 在 Worker 里跑」
→ Scheduler 与 React 渲染都在【主线程】;Worker 只用于「把用户的重计算移出去」
⑭ 认为「`flushSyncCallbacks` 只在 flushSync 时用」
→ 它同时用于「SyncLane 的微任务冲洗」与「commit 结束后的同步回调」
setState 之后到界面更新的完整链路。」requestEventTime() 取时间、requestUpdateLane() 选车道、createUpdate() 建更新对象、enqueueUpdate() 把它挂到 fiber.updateQueue(环形链表);② 调度:scheduleUpdateOnFiber() → markRootUpdated() 在根上标记车道 → ensureRootIsScheduled() 用 getNextLanes() 算出待处理车道并去重调度;③ 分路:若是 SyncLane → scheduleMicrotask(flushSyncCallbacks)(微任务里同步渲染,这就是批处理);否则 → lanesToEventPriority() 翻译成 Scheduler 优先级 → scheduleCallback(performConcurrentWorkOnRoot);④ 渲染(renderRootConcurrent + workLoopConcurrent,每处理一个 fiber 检查 shouldYield(),可中断);⑤ 提交(commitRootImpl:flushPassiveEffects → commitBeforeMutationEffects → commitMutationEffects → root.current = finishedWork → commitLayoutEffects → 调度 passive effects)。
「Scheduler 为什么用 MessageChannel 而不是 requestIdleCallback?」
因为 requestIdleCallback 有三个致命问题:① 兼容性差(Safari 长期不支持);② 调用频率被限制(规范允许「最多约 20 次/秒」,相邻回调间隔可能很长);③ 「空闲时间」由浏览器决定,不可控——而 React 需要稳定的 5ms 时间片来「让出后及时恢复」。requestAnimationFrame 的问题是「后台标签页会暂停」且「一帧只调一次」;setTimeout 有「嵌套 4ms 最小延迟」且后台被节流到 1s。而 MessageChannel 是宏任务(比 setTimeout 更及时)、所有主流浏览器都支持,所以成为默认实现。
「为什么 SyncLane 要用微任务而不是直接渲染?」
因为「直接渲染」会让 setState 变成「立即、同步」的语义——那样「一次点击里的三次 setState」就会触发三次渲染,而不是批处理成一次。用 scheduleMicrotask(flushSyncCallbacks) 的含义是「等当前同步代码跑完之后再统一渲染」——这正是批处理的实现基础(也解释了「为什么事件处理里读不到新值」,见 12.2)。
「哪些更新不可以中断?」
performConcurrentWorkOnRoot 里的判断是:shouldTimeSlice = !includesBlockingLane(lanes) && !includesExpiredLane(root, lanes) && !didTimeout。所以三种情况走 renderRootSync(不可中断):① 包含阻塞车道(SyncLane、InputContinuousLane——即点击、输入这类离散/连续事件);② 车道已过期(低优先级任务等太久 → 必须强制做完,避免饿死);③ Scheduler 告知 didTimeout。其余情况(如 DefaultLane、TransitionLanes)才走可中断的并发渲染。
「root.current = finishedWork 为什么在 mutation 之后、layout 之前?」
因为这两个阶段对「树」的视角不同:mutation 阶段正在把「新树的 DOM」写进文档,此时读取操作(如 getSnapshotBeforeUpdate、以及内部依赖 current 的逻辑)仍然应该看到旧树;DOM 变更完成之后再切换 current,随后的 layout 阶段(赋 ref、useLayoutEffect)才看到新树。这个「切换时机」是「双缓存 + 副作用顺序」的关键衔接点。
「低优先级任务会不会被『饿死』?」
不会——因为每个更新都有过期时间(expirationTime,由 priorityLevel 决定,如 userBlocking: 250ms、normal: 5000ms)。一旦过期,React 会在 includesExpiredLane() 里检测到并强制同步执行(走 renderRootSync,不再让出)。所以低优先级更新「可能很晚」但一定会执行。
「render 阶段为什么必须纯?从源码角度看。」
因为 workLoopConcurrent 可能「在任意一个 performUnitOfWork 之后让出」——让出后,如果有更高优先级的更新插进来,React 会「丢弃当前 workInProgress 的部分成果并重新开始渲染」。也就是说 render 阶段的执行次数是不确定的(可能 0 次、1 次、多次)。所以任何副作用(改外部变量、发请求、读写 DOM)都会重复执行或产生不一致。这也是 StrictMode 双调用的理论基础(见 8.5)。
「fiber.updateQueue 为什么用环形链表?」
两个原因:① O(1) 追加——维护一个 shared.pending 指针,新更新插到「pending 的末尾」,不需要遍历;② 批量消费——渲染时把 pending 切成一段独立链表(pending → pending.next 断开),消费完再对接回去。这样「渲染期间新来的更新」不会影响「本次正在消费的批次」,保证「一次渲染处理一个确定的更新集合」(这也与「优先级车道」配合,让高优先级更新能先被处理)。
链路形状:
updateContainer(requestEventTime+requestUpdateLane+createUpdate+enqueueUpdate(环形链表))→scheduleUpdateOnFiber(markRootUpdated+ensureRootIsScheduled)→ 分路(SyncLane走 **微任务flushSyncCallbacks,其它走lanesToEventPriority→scheduleCallback)→ Scheduler(MessageChannel + 5ms 时间片 +shouldYieldToHost) →performConcurrentWorkOnRoot(shouldTimeSlice判断阻塞车道/过期/超时)→workLoopConcurrent(while (workInProgress && !shouldYield()) performUnitOfWork,beginWork递 /completeWork归,可中断)→commitRootImpl(flushPassiveEffects→ before mutation → mutation(含useInsertionEffect)→root.current = finishedWork→ layout(useLayoutEffect)→ 调度 passive);三个关键结论:①SyncLane用微任务是为了批处理、② 阻塞车道 / 过期车道 /didTimeout不可中断、③ 低优先级有expirationTime所以不会被饿死;Scheduler 用MessageChannel而非requestIdleCallback(兼容性 + 频率限制 + 空闲时间不可控);render 可能被重放/丢弃 → 必须纯**。


最新评论
读过书不知道欧·亨利的人少。教科书上选文有
这小生活不错呀
不错,必须顶一下!
看着你还在坚持,很好
看来忙了也没时间更新博客了
NIce。学习了。。。。
网站不错!!!!
简洁实用,好文章!