Scheduler 调度原理
前端React
调度初始化
每次更新都会如下图调用:
setState() ↓ enqueueUpdate() // 把 update 放进 Fiber ↓ scheduleUpdateOnFiber() // 通知整棵树需要更新 ↓ ensureRootIsScheduled() // 决定要不要注册调度任务 ↓ unstable_scheduleCallback() // Scheduler 阶段真正注册 task 的入口
在 Scheduler 阶段会做:
计算任务的
timeout → 创建一个 task 对象 → 放入 taskQueue → 若当前没有调度在跑 → 发起一次调度(MessageChannel)Scheduler 阶段的 Event Loop
假设有大量
setState 需要更新 Fiber,React 不会一次算完,而是分段:Frame 1: 宏任务: performWorkUntilDeadline() - 设置 deadline = now + 5ms - 执行一部分 render (比如 2ms) - 达到 5ms → 应让出线程 → 停止 - 剩下的工作没做完 → 再 postMessage(null) 浏览器: layout + paint Frame 2: 宏任务: performWorkUntilDeadline() - deadline = now + 5ms - 再执行一点 render(比如 4ms) - 又到 5ms → 停止 - 剩下的继续 → 再 postMessage(null) 浏览器: layout + paint Frame 3: 宏任务: performWorkUntilDeadline() ... (直到任务全部完成)
每 5ms 执行一小段任务,保证浏览器有绘制机会。
Scheduler 的 workLoop
与
Reconciler 内部的 Fiber 渲染循环不同,Scheduler 的 workLoop 决定的是:在当前这一次调度中,React 要从任务队列中执行多少个调度任务(task),以及是否因为时间片耗尽而提前结束本轮调度。while (还有 task) { 如果任务没过期 && shouldYieldToHost(): 停止(时间片用完) else: 执行这个 task.callback ,构建新的 fiber 如果 callback 返回 continuationCallback:// Fiber工作量太大,超出5ms 下次继续这个 task(可中断渲染) 否则: 从队列移除 } 返回:queue 是否清空(告诉 Scheduler 还要不要下一轮)
可中断渲染 (Fiber 的 workLoop)
Fiber 的 workLoop 也会检查是否超过时间片,但时间片判断的
shouldYieldToHost() 方法来自 Scheduler。节流防抖(Throttle + Debounce)
在 Reconciler 注册调度任务(Scheduler callback)时,如果已经存在一个尚未执行的 scheduler callback:
优先级相同 → 节流
不再注册新的调度任务,继续复用已有调度任务,所有 setState 产生的 update 仍然会被保留,不会丢失
优先级更高 → 防抖
取消旧的调度任务,注册一个新的高优先级调度任务,依然不会丢掉任何 update,只影响调度顺序与调度时机
总而言之,是调度任务的节流防抖,但是并不会丢失
update 。