进入调度 scheduleCallback
首先上一下 流程图,

我们还是将同步任务和异步任务分开讲解
一、同步更新的流程(isSync
分支)
同步更新是指立即执行 的更新任务(如用户交互事件、强制同步场景),核心逻辑是:任务直接进入任务队列,由 workLoop
立即执行。
流程步骤:
- 判断同步更新 :当任务到达
一次新的更新任务
时,判断是否为同步更新(如isSync === true
)。 - 创建任务 :同步更新需先通过
schedule_callback
创建一个task
(任务对象)。 - 进入任务队列 :将
task
直接放入taskQueue
(或timeQueue
,后续由advanceTimers
合并到taskQueue
)。 - 触发帧执行 :通过
requestHostCallback
请求浏览器"下一帧",让workLoop
调度执行任务。 - 执行更新 :
workLoop
从taskQueue
中取出任务并执行,完成同步更新。
我们先解释一下 几个名词:
taskQueue
: 里面存的都是过期的任务,依据任务的过期时间 (expirationTime)排序,需要在调度的 workLoop 中循环执行完这些任务。
timerQueue
: 里面存的都是没有过期的任务,依据任务的开始时间 (startTime)排序,在调度 workLoop 中会用 advanceTimers 检查任务是否过期,如果过期了,放入 taskQueue 队列。
这里要明确一件事:React 的更新任务最后都是放在 taskQueue 中的
为什么同步任务会直接把任务放进 taskQueue 呢?
优先级机制的绕过
同步任务默认标记为最高优先级(如 ImmediatePriority
),其 expirationTime
设置为 -1
-已经过期,强制 workLoop
优先处理,无需参与 timeQueue
的优先级排序
也就是它直接会走 超时分支,
二、非同步更新的流程(isSync === false
分支)
非同步更新是指延迟执行 的更新任务(如批量更新、异步调度场景),核心逻辑是:任务先按优先级和超时规则排队,最终由 requestHostCallback
触发 workLoop
执行。
流程步骤:
-
判断非同步更新 :当任务到达
一次新的更新任务
时,判断是否为非同步更新(如isSync === false
)。 -
设置优先级 :通过
priorityLevel
为任务分配优先级(如user-blocking
、normal
等)。 -
创建任务 :通过
schedule_callback
创建一个task
,任务携带优先级、回调等信息。 -
超时检测:检查任务是否"超时"(如优先级高但等待时间过长)。
- 若超时 :直接进入
超时任务处理
,将task
放入taskQueue
,后续由workLoop
执行。 - 若未超时 :将
task
放入timeQueue
(按优先级和超时规则排队)。
- 若超时 :直接进入
-
触发延迟执行 :通过
requestHostTimeout
请求延迟时间后执行handleTimeout
。 -
处理延迟任务 :
handleTimeout
调用advanceTimers
,将timeQueue
中到期的任务移入taskQueue
。 -
触发帧执行 :通过
requestHostCallback
请求浏览器"下一帧",让workLoop
调度执行任务。 -
执行更新 :
workLoop
从taskQueue
中取出任务并执行,完成非同步更新。
再科普几个函数
requestHostTimeout
当一个任务没有超时,那么 React 把它放入 timerQueue 中了,但是它什么时候执行呢?
这个时候Schedule 用 requestHostTimeout 让 一 个 未 过 期 的 任 务 能 够 到 达 恰 好 过 期 的 状 态,那 么 需 要 延 迟startTime-currentTime 毫秒就可以了。requestHostTimeout 就是通过 setTimeout 来进行延时指定时间的。
js
requestHostTimeout = function (cb, ms) {
_timeoutID = setTimeout(cb, ms);};
cancelHostTimeout = function () {clearTimeout(_timeoutID);
};
requestHostTimeout 延时执行 handleTimeout,cancelHostTimeout 用于清除当前的延时器。 也就是穿的cb就是 handleTimeout
handleTimeout
延时指定时间后,调用的 handleTimeout 函数会把任务重新放在 requestHostCallback 调度。
js
function handleTimeout(){
isHostTimeoutScheduled = false;
/* 将 timeQueue 中过期的任务,放在 taskQueue 中。*/
advanceTimers(currentTime);
/* 如果没有处于调度中 */
if(!isHostCallbackScheduled){
/* 判断有没有过期的任务,*/
if (peek(taskQueue) ! = = null) {
isHostCallbackScheduled = true;
/* 开启调度任务 */
requestHostCallback(flushWork);
}
}
}
requestHostCallback
js
let scheduledHostCallback = null
/* 建立一个消息通道 */
var channel = new MessageChannel();
/* 建立一个 port 发送消息 */
var port = channel.port2;
channel.port1.onmessage = function(){
/* 执行任务 */
scheduledHostCallback()
/* 执行完毕,清空任务 */
scheduledHostCallback = null
};
/* 向浏览器请求执行更新任务 */
requestHostCallback = function (callback) {
scheduledHostCallback = callback;
if (!isMessageLoopRunning) {
isMessageLoopRunning = true;
port.postMessage(null);
}
};
在一次更新中,Scheduler 会调用 requestHostCallback,把更新任务赋值给 scheduledHostCallback,然后 port2 向 port1 发起 postMessage 消息通知。
port1 会通过 onmessage,接受来自 port2 的消息,然后执行更新任务 scheduledHostCallback
在 Scheduler 中就是通过如上 MessageChannel 的方式向浏览器请求是否有空闲时间执行下一个更新任务(scheduledHostCallback)
所以那么调度 requestHostCallback。本质上调度的是 flushWork(依次更新过期任务队列中的任务)。
Concurrent 模式如何中断渲染
在React的异步渲染流程中,当Reconciler进入异步workLoop后,会通过shouldYield来判断是否需要中断当前任务。
shouldYield的核心原理是基于时间切片机制,它会检查当前任务的执行时间是否超过了规定的时间片(通常约为5ms)。
若执行时间未超过时间片,且没有更高优先级的任务插入,shouldYield返回false,当前任务继续执行;
若执行时间超出时间片,或者有更高优先级的任务(如用户交互等)需要处理,shouldYield返回true,此时当前任务会被中断, Reconciler会将控制权交还给浏览器,让浏览器进行事件处理、Layout、Paint等操作。
待浏览器空闲后,Scheduler会重新调度被中断的任务,恢复workLoop继续执行剩余的任务单元。这种机制实现了任务的可中断和恢复,避免了长时间占用主线程导致的页面卡顿,保证了应用的流畅性。
好了,scheduler 异步调度原理 我们就这样讲完了,如果你能把这张图盲画出来就完美了,后面我们要讲讲hook了,下篇文章见!