Scheduler 最小堆 MessageChannel 时间片检查它的目标只有一个推进任务但永远别饿死浏览器。调度任务React 19 最新 Scheduler 源码**packages/scheduler** 中普通任务非 Immediate同步优先级之外的任务的完整调度链unstable_scheduleCallback ↓ requestHostCallback ↓ schedulePerformWorkUntilDeadline ↓ performWorkUntilDeadline ↓ flushWork ↓ workLoop ↓ shouldYieldToHost ↓ advanceTimersScheduler 本质是一个“带时间片控制的最小堆 宏任务驱动循环”调度器。一、调度的起点unstable_scheduleCallback源码位置React 19/packages/scheduler/src/forks/Scheduler.jslet getCurrentTime () performance.now(); // 为所有任务维护了连个最小堆每次从队列里面取出来的都是优先级最高时间即将过期 // timerQueue // 延时任务队列task.startTime now —— 按 startTime 排序延迟最小的先看 // taskQueue // 立即可执行的任务task.startTime now—— 按 expirationTime 排序最紧急的先执行 // Timeout 对应的值 var IMMEDIATE_PRIORITY_TIMEOUT -1; var USER_BLOCKING_PRIORITY_TIMEOUT 250; var NORMAL_PRIORITY_TIMEOUT 5000; var LOW_PRIORITY_TIMEOUT 10000; var IDLE_PRIORITY_TIMEOUT maxSigned31BitInt; // 1073741823 /** * 调度任务的入口 * param {*} priorityLevel 优先级等级 * param {*} callback 任务回调函数 * param {*} options { delay: number } 该对象有 delay 属性表示要延迟的时间决定 expirationTime * returns */ function unstable_scheduleCallback(priorityLevel, callback, options) { // 获取当前的时间 var currentTime getCurrentTime(); var startTime; // 设置起始时间 startTime如果有延时 delay起始时间需要添加上这个延时否则起始时间就是当前时间 if (typeof options object options ! null) { var delay options.delay; if (typeof delay number delay 0) { startTime currentTime delay; } else { startTime currentTime; } } else { startTime currentTime; } var timeout; // 根据传入的优先级等级来设置不同的 timeout switch (priorityLevel) { case ImmediatePriority: timeout IMMEDIATE_PRIORITY_TIMEOUT; // -1 break; case UserBlockingPriority: timeout USER_BLOCKING_PRIORITY_TIMEOUT; // 250 break; case IdlePriority: timeout IDLE_PRIORITY_TIMEOUT; // 1073741823 break; case LowPriority: timeout LOW_PRIORITY_TIMEOUT; // 10000 break; case NormalPriority: default: timeout NORMAL_PRIORITY_TIMEOUT; // 5000 break; } // 接下来就计算出过期时间 // 只有 ImmediatePriority 任务 比当前时间要早其他任务都会不同程度的延迟 var expirationTime startTime timeout; // 创建一个新的任务 var newTask { id: taskIdCounter, // 任务 id callback, // 执行任务回调函数 export type Callback boolean ?Callback; priorityLevel, // 任务的优先级 startTime, // 任务开始时间 expirationTime, // 任务的过期时间 sortIndex: -1, // 用于小顶堆优先级排序始终从任务队列中拿出最优先的任务 }; if (enableProfiling) { newTask.isQueued false; } if (startTime currentTime) { // 说明这是一个延时任务 newTask.sortIndex startTime; // 将该任务推入到 timerQueue 的任务队列中 push(timerQueue, newTask); if (peek(taskQueue) null newTask peek(timerQueue)) { // 说明 taskQueue 里面的任务已经全部执行完毕然后从 timerQueue 里面取出一个优先级最高的任务作为此时的 newTask if (isHostTimeoutScheduled) { cancelHostTimeout(); } else { isHostTimeoutScheduled true; } // 如果是延时任务调用 requestHostTimeout 进行延时任务的调度 requestHostTimeout(handleTimeout, startTime - currentTime); } } else { // 说明不是延时任务 newTask.sortIndex expirationTime; // 设置了 sortIndex 后在任务队列里面进行一个排序 push(taskQueue, newTask); if (enableProfiling) { markTaskStart(newTask, currentTime); newTask.isQueued true; } // 最终调用 requestHostCallback 进行普通任务的调度 if (!isHostCallbackScheduled !isPerformingWork) { isHostCallbackScheduled true; requestHostCallback(); } } // 向外部返回任务 return newTask; }它的职责是根据优先级计算 timeout构造一个 Task 对象放入任务到对应的最小堆请求宿主开始调度requestHostCallback 普通任务 / requestHostTimeout 延时任务任务执行时刻IMMEDIATE_PRIORITY_TIMEOUT - currentTime - USER_BLOCKING_PRIORITY_TIMEOUT - IDLE_PRIORITY_TIMEOUT二、普通任务调用 requestHostCallback当普通任务进入taskQueue后调用requestHostCallback然后调用schedulePerformWorkUntilDeadline/** * * param {*} callback 是在调用的时候传入的 flushWork * requestHostCallback 这个函数没有做什么事情主要就是调用 schedulePerformWorkUntilDeadline */ function requestHostCallback() { if (!isMessageLoopRunning) { isMessageLoopRunning true; schedulePerformWorkUntilDeadline();// 实例化 MessageChannel 进行后面的调度 } } let schedulePerformWorkUntilDeadline; // undefined if (typeof localSetImmediate function) { // Node.js and old IE. schedulePerformWorkUntilDeadline () { localSetImmediate(performWorkUntilDeadline); }; } else if (typeof MessageChannel ! undefined) { // 大多数情况下使用的是 MessageChannel const channel new MessageChannel(); const port channel.port2; channel.port1.onmessage performWorkUntilDeadline; schedulePerformWorkUntilDeadline () { port.postMessage(null); }; } else { // setTimeout 进行兜底 schedulePerformWorkUntilDeadline () { localSetTimeout(performWorkUntilDeadline, 0); }; }schedulePerformWorkUntilDeadline根据不同的环境选择不同的生成宏任务的方式。大多数都是 MessageChannel 每一次调度推进都是一个新的宏任务浏览器中间有机会 paint三、延时任务调用 requestHostTimeout handleTimeoutReact Scheduler 不直接使用 setTimeout而是抽象成 HostConfig可在不同环境实现。requestHostTimeout(callback, ms)这个函数会安排一个底层超时回调。在浏览器环境下它一般等价于const timeoutID setTimeout(callback, ms);注意这是 严格意义上的延时调度用于把 timerQueue 的任务唤醒进 taskQueue。requestHostTimeout 实际上就是调用 setTimoutout然后在 setTimeout 中调用传入的 handleTimeout。注意延时任务只需要“不会提前执行”而不需要“精准执行”所以这里使用了 setTimeoutsetTimeout 只是负责“唤醒” Scheduler不负责精度和优先级expirationTime 控制控制鉴于负责延时和必须是宏任务的特性这里使用 setTimeout 最合适。React 的调度精度来自MessageChannel 时间片检查 expirationTime 。handleTimeout 是真正被 setTimeout 调用的函数function handleTimeout(currentTime) { // 遍历 timerQueue将时间已经到了的延时任务放入到 taskQueue advanceTimers(currentTime); if (!isHostCallbackScheduled) { if (taskQueue.length 0) { // 采用调度普通任务的方式进行调度 requestHostCallback(flushWork); } else { // 如果任务仍然是延时继续设置 HostTimeout const firstTimer peek(timerQueue); if (firstTimer ! null) { const nextDelay firstTimer.startTime - currentTime; requestHostTimeout(handleTimeout, nextDelay); } } } }四、performWorkUntilDeadline这是 Scheduler 的“驱动心跳”。let startTime -1; const performWorkUntilDeadline () { if (enableRequestPaint) { needsPaint false; } if (isMessageLoopRunning) { const currentTime getCurrentTime(); // 这里的 startTime 并非 unstable_scheduleCallback 方法里面的 startTime // 而是一个全局变量默认值为 -1 // 用来测量任务的执行时间从而能够知道主线程被阻塞了多久 startTime currentTime; let hasMoreWork true; try { // flushWork 为任务中转本质上内部继续调用 workLoop 判断任务执行情况 hasMoreWork flushWork(currentTime); } finally { if (hasMoreWork) { // 上面刚刚讲过的方法根据不同的环境选择不同的生成宏任务的方式MessageChannel schedulePerformWorkUntilDeadline(); } else { isMessageLoopRunning false; } } } };它做三件事设置本次调度的时间起点调用 flushWork如果还有任务 → 再发一个 MessageChannel五、flushWork 和 workLoop 执行任务循环// flushWork 是个中转它真正执行的是 workLoop function flushWork(initialTime) { // ... // 各种开关、报错捕获... // 其实核心就是 workLoop return workLoop(initialTime); } // 不断从任务队列中取出任务执行 function workLoop(initialTime: number) { // initialTime 开始执行任务的时间 let currentTime initialTime; // advanceTimers 是用来遍历 timerQueue判断是否有已经到期的任务 // 如果有将这个任务放入到 taskQueue advanceTimers(currentTime); currentTask peek(taskQueue); while (currentTask ! null) { if (!enableAlwaysYieldScheduler) { if (currentTask.expirationTime currentTime shouldYieldToHost()) { // 任务没有过期并且需要中断任务归还主线程 break; } } // 返回任务本身致使任务之后可以接着继续执行 const callback currentTask.callback; if (typeof callback function) { currentTask.callback null; currentPriorityLevel currentTask.priorityLevel; const didUserCallbackTimeout currentTask.expirationTime currentTime; if (enableProfiling) { markTaskRun(currentTask, currentTime); } // 执行任务其他判断都是做“阶段过程资料收集”无需关注 const continuationCallback callback(didUserCallbackTimeout); currentTime getCurrentTime(); if (typeof continuationCallback function) { currentTask.callback continuationCallback; if (enableProfiling) { markTaskYield(currentTask, currentTime); } advanceTimers(currentTime); return true; } else { if (enableProfiling) { markTaskCompleted(currentTask, currentTime); currentTask.isQueued false; } if (currentTask peek(taskQueue)) { pop(taskQueue); } advanceTimers(currentTime); } } else { pop(taskQueue); } // 执行完再从 taskQueue 中取出一个任务 currentTask peek(taskQueue); if (enableAlwaysYieldScheduler) { if (currentTask null || currentTask.expirationTime currentTime) { break; } } } // 如果任务不为空那么还有更多的任务hasMoreTask 为 true if (currentTask ! null) { return true; } else { // taskQueue 这个队列空了那么我们就从 timerQueue 里面去看延时任务 const firstTimer peek(timerQueue); if (firstTimer ! null) { requestHostTimeout(handleTimeout, firstTimer.startTime - currentTime); } // 没有进入上面的 if说明 timerQueue 里面的任务也完了返回 false外部的 hasMoreWork 拿到的就也是 false return false; } }任务是否可以被打断shouldYieldToHost()如果返回 true当前宏任务结束重新发 MessageChannel浏览器可以渲染六、shouldYieldToHost —— 时间片控制核心源码核心function shouldYieldToHost() { const timeElapsed getCurrentTime() - startTime; if (timeElapsed frameInterval) { return false; } return true; }其中frameInterval 5ms注意React 不等 16ms 一整帧而是每 5ms 检查一次为什么预留给浏览器 layout paint预留给输入事件七、advanceTimers —— 延迟任务晋升机制每次 workLoop 结束后会调用advanceTimers(currentTime);。它会检查 timerQueue把到时间的任务移动到 taskQueuefunction advanceTimers(currentTime) { // 取出 startTime currentTime 的 timer let timer peek(timerQueue); while (timer ! null timer.startTime currentTime) { pop(timerQueue); timer.sortIndex timer.expirationTime; push(taskQueue, timer); timer peek(timerQueue); } }这保证延迟任务不会饿死八、Scheduler 与 Fiber 的关系Scheduler 不知道 Fiber。它只负责“什么时候调用 callback”而 callback 通常是performConcurrentWorkOnRoot那里面才是beginWorkcompleteWorkcommitRoot九、任务拆分的思路来源假设有这样一段源码unstable_scheduleCallback(NormalPriority, () { heavyWork(); // 10ms });如果heavyWork()是同步执行 10msScheduler无法中断它因为 JS 是单线程的函数执行过程中无法被抢占。来看源码核心逻辑const continuation callback(); if (typeof continuation function) { currentTask.callback continuation; } else { pop(taskQueue); }如果 callback 返回一个函数Scheduler 会把它当成“任务的下一段”。举一个最小可理解示例// 假设这是一个“大”任务 function createBigTask(total) { let i 0; function work() { while (i total) { console.log(i); if (shouldStopManually()) { return work; // 返回自身表示还有后续 } } return null; // 完成 } return work; }调度unstable_scheduleCallback( NormalPriority, createBigTask(1000) );执行workLoop() ↓ 执行 work() ↓ 执行到一部分 ↓ 返回 work continuation ↓ Scheduler 保存 callback work ↓ shouldYieldToHost() 为 true ↓ break ↓ 下一帧继续执行在 React 里被拆分的任务不是“普通函数”而是performConcurrentWorkOnRoot。它内部会调用workLoopConcurrent() 。核心逻辑简化版while ( workInProgress ! null !shouldYield() ) { performUnitOfWork(workInProgress); }performUnitOfWork 只做一个 Fiber 节点function performUnitOfWork(unit) { const next beginWork(unit); if (next null) { completeUnitOfWork(unit); } return next; }也就是说React 把整棵 Fiber 树拆成一个个“节点单位”/“任务切片”。更形象的可以表示为一个组件树App ├─ Header ├─ Content │ ├─ List │ └─ Sidebar └─ Footer会被拆成performUnitOfWork(App) performUnitOfWork(Header) performUnitOfWork(Content) performUnitOfWork(List) performUnitOfWork(Sidebar) performUnitOfWork(Footer)每个 Fiber 节点就是一个小任务。回到 Scheduler 这段判断if ( currentTask.expirationTime currentTime shouldYieldToHost() ) { break; }当时间片用完时break 。此时workLoop 停止任务还没完成currentTask.callback 仍然是 performConcurrentWorkOnRoot下一帧MessageChannel ↓ performWorkUntilDeadline ↓ flushWork ↓ workLoop ↓ 继续执行 performConcurrentWorkOnRoot延时任务并不会立刻进入 taskQueue而是先进入 timerQueue等待被“唤醒”然后再调度。十、完整调度流程下面是一个完整的流程图unstable_scheduleCallback() ↓ 是否延时? (startTime now) / \ 是 否 ↓ ↓ push(timerQueue) push(taskQueue) ↓ ↓ requestHostTimeout(requestHostCallback) ↓ ↓ 等待 Timer requestHostCallback ↓ ↓ timeout 到时 MessageChannel ↓ ↓ handleTimeout performWorkUntilDeadline ↓ ↓ advanceTimers flushWork ↓ ↓ timerQueue → taskQueue ↓ workLoop ↓ shouldYieldToHost? ↓ 是 否 notifyBrowserPaint 执行 Callback ↓ callback 返回 continuation? ↓ 是 否 更新 Task.callback pop(taskQueue) ↓ 可能再次调度示例理解示例 1普通任务正常进入队列unstable_scheduleCallback( NormalPriority, () console.log(normal) );步骤now startTime (0 delay)放入 taskQueuerequestHostCallback(flushWork)MessageChannel 触发 performWorkUntilDeadlineflushWork → workLoop执行 tasktaskQueue 空 → 调度结束示例 2延时任务未来才执行unstable_scheduleCallback( NormalPriority, () console.log(delayed), { delay: 1000 } );步骤t0 ↓ startTime now 1000 push(timerQueue) requestHostTimeout(handleTimeout, 1000)1s 后handleTimeout 被 setTimeout 调用 ↓ advanceTimers → 取出 timerQueue 里所有 startTime 1s 的任务 → 推到 taskQueue taskQueue 变非空 ↓ requestHostCallback(flushWork) ↓ MessageChannel 回调 ↓ flushWork → workLoop → task 执行示例 3延时任务到了中间又被打断unstable_scheduleCallback( NormalPriority, step1, { delay: 1000 } ); function step1() { console.log(step1); return step2; } function step2() { console.log(step2); }执行过程t0 → timerQueue 入队 t1000 → handleTimeout → advanceTimers → taskQueue ↓ MessageChannel ↓ workLoop 执行 step1 延时任务 ↓ step1 返回 continuation step2 延时任务 ↓ 此时 workLoop 检查 shouldYieldToHost 如果 time片到 → 中断 → callback (continuation) 留在 taskQueue → requestHostCallback 执行普通任务 下次继续执行 step2 延时任务设计哲学① 延时任务不能抢占浏览器渲染如果立即调度setTimeout(() {}), Promise.then(...)React 可能会阻塞页面渲染所以必须分开先 timerQueue 等待 再 taskQueue 才跑② 延时任务按照优先级即使 delay 到了expirationTime startTime timeout如果更高优先级任务进入 taskQueueReact 会先执行高优先级的。delay控制什么时候进入 taskQueue而expirationTime控制进入 taskQueue 之后的优先级排序。③ extendable 继续推进任务一个任务返回 continuation 时currentTask.callback continuation;并且还可能继续 schedule。