React-Scheduler 调度器如何掌控主线程?

37 阅读4分钟

前言

在 React 18 的并发时代,Scheduler(调度器) 是实现非阻塞渲染的幕后英雄。它不只是 React 的一个模块,更是一个通用的、高性能的 JavaScript 任务调度库。它不仅让 React 任务可以“插队”,还让“长任务”不再阻塞浏览器 UI 渲染。

一、 核心概念:什么是 Scheduler?

Scheduler 是一个独立的包,它通过与 React 协调过程(Reconciliation)的紧密配合,实现了任务的可中断、可恢复、带优先级执行。

主要职责

  1. 优先级管理:根据任务紧急程度(如用户点击 vs 数据预取)安排执行顺序。
  2. 空闲时间利用:在浏览器每一帧的空闲时间处理不紧急的任务。
  3. 防止主线程阻塞:通过“时间片(Time Slicing)”机制,避免长任务导致页面假死。

二、 Scheduler 的完整调度链路

当一个 setState 触发后,Scheduler 内部会经历以下精密流程:

1. 任务创建与通知

当状态更新时,React 不会立即执行 Render。它首先会创建一个 Update对象来记录这次变更,这个对象中包含这次更新所需的全部信息,例如更新后的状态值,Lane车道模型分配的任务优先级.

2. 优先级排序与队列维护

  • 任务优先级排序: 创建更新后,react会调用scheduleUpdateOnFiber函数通知scheduler调度器有个一个新的任务需要调度,这时scheduler会对该任务确定一个优先级,以及过期时间(优先级越高,过期时间越短,表示越紧急)

  • 队列维护: 接着scheduler会将该任务放入到循环调度中,scheduler对于任务循环调度在内部维护着两个队列,一个是立即执行队列taskQueue和延迟任务队列timeQueue,新任务会根据优先级进入到相应对列

    • timerQueue(延时任务队列) :存放还未到开始时间的任务,按开始时间排序。
    • taskQueue(立即任务队列) :存放已经就绪的任务,按过期时间排序。优先级越高,过期时间越短。

3. 时间片的开启:MessageChannel

将任务放入队列后,scheduler会调用requetHostCallback函数去请求浏览器在合适的时机去执行调度,该函数通过 MessageChannel对象中的port.postMessage 方法创建一个宏任务,浏览器在下一个宏任务时机触发 port.onmessage,并在这宏任务回调中启动 workLoop函数。

补充:Scheduler 会调用 requestHostCallback 请求浏览器调度。它没有选择 setTimeout,而是选择了 MessageChannel

为什么选 MessageChannel? setTimeout(fn, 0) 在浏览器中通常有 4ms 的最小延迟,且属于宏任务中执行时机较晚的。MessageChannelport.postMessage 产生的宏任务执行时机更早,且能更精准地在浏览器渲染帧之间切入。

4. 工作循环:workLoop

  • 在宏任务回调中,调度器会进入 workLoop。它会调用performUnitOfWork函数循环地处理Fiber节点,对比新旧节点的props、state,并从队列中取出最紧急的任务交给 React 执行。

  • workLopp中会包含一个shouldYield函数中断检查函数,用于检查当前时间片是否耗尽以及是否有更高优先级的任务执行,如果有的话则会将主线程控制权交还给浏览器,以保证高优先级任务(如用户输入、动画)能及时响应。


5. 中断与恢复:shouldYield 的魔力

workLoop 执行过程中,每一项单元工作完成后,都会调用 shouldYield() 函数进行“路况检查”。

  • 中断条件:如果当前时间片(通常为 5ms)耗尽,或者检测到有更紧急的用户交互(高优任务插队),shouldYield 返回 true
  • 状态保存:此时 React 会记录当前 workInProgress 树的位置,将控制权交还给浏览器。
  • 任务恢复:Scheduler 会在下一个时间片通过 MessageChannel 再次触发,从记录的位置继续执行,从而实现可恢复。

6. 任务插队

如果在执行一个低优先级任务时,有高优先级任务加入(如用户突然点击按钮),Scheduler会中断当前的低优任务并记录该位置,先执行高优任务。等高优任务完成后,再重新执行或继续之前的低优任务


三、 补充

  1. 执行时机对比MessageChannel 确实在宏任务中非常快,但在某些极其特殊的情况下(如没有 MessageChannel 的旧环境),它会回退到 setTimeout
  2. 饥饿现象防止:如果一个低优先级任务一直被插队怎么办?Scheduler 通过过期时间解决。一旦任务过期,它会从 taskQueue 中被提升为同步任务,强制执行。