Skip to content

第 4.2 节:任务的优先级:不同场景下的调度策略

Scheduler 的强大之处不仅在于时间分片,更在于它能够区分任务的轻重缓急。并非所有任务都是生而平等的:用户的点击响应显然比页面底部一个非关键组件的渲染更重要。Scheduler 通过一个精细的优先级系统,确保高优先级的任务能够“插队”,从而在不同场景下提供最优的用户体验。

1. 五个优先级等级

React Scheduler 定义了五个离散的优先级等级,这些定义位于 packages/scheduler/src/SchedulerPriorities.js 中。

javascript
// packages/scheduler/src/SchedulerPriorities.js

export const NoPriority = 0;
export const ImmediatePriority = 1;      // 立即执行
export const UserBlockingPriority = 2;   // 用户阻塞级别
export const NormalPriority = 3;         // 普通级别
export const LowPriority = 4;            // 低级别
export const IdlePriority = 5;           // 空闲级别

这些优先级决定了任务的紧急程度,从 ImmediatePriority(最紧急)到 IdlePriority(最不紧急)。

2. 优先级如何决定调度策略?

优先级的核心作用是计算任务的过期时间 (expirationTime)。一个任务的过期时间越早,它在 taskQueue(一个最小堆)中的排序就越靠前,也就越先被执行。

过期时间的计算公式是:

expirationTime = startTime + timeout

这里的 startTime 是任务被创建的时间,而 timeout 则完全由任务的 priorityLevel 决定。unstable_scheduleCallback 函数中的 switch 语句清晰地揭示了这一点:

javascript
// packages/scheduler/src/forks/Scheduler.js

function unstable_scheduleCallback(priorityLevel, callback, options) {
  // ...
  var timeout;
  switch (priorityLevel) {
    case ImmediatePriority:
      timeout = -1; // 立即过期
      break;
    case UserBlockingPriority:
      timeout = 250; // 250ms 后过期
      break;
    case IdlePriority:
      timeout = 1073741823; // 永不过期(一个极大值)
      break;
    case LowPriority:
      timeout = 10000; // 10s 后过期
      break;
    case NormalPriority:
    default:
      timeout = 5000; // 5s 后过期
      break;
  }

  var expirationTime = startTime + timeout;
  // ...
}

让我们逐一分析每个优先级的应用场景和调度策略。

ImmediatePriority (立即优先级)

  • Timeout: -1ms
  • 策略:立即过期。这意味着任务必须在当前事件循环中被同步执行,不能被延迟。
  • 应用场景
    • 受控的用户输入:例如,输入框的 onChange 事件。为了保证用户输入和界面反馈的同步,这类更新必须立即执行。
    • flushSync:当开发者使用 flushSync API 包装一个更新时,该更新会被赋予立即优先级。

UserBlockingPriority (用户阻塞优先级)

  • Timeout: 250ms
  • 策略:任务在 250ms 后过期。它应该被尽快执行,但可以被短暂延迟。Scheduler 会尝试在 250ms 内完成它。
  • 应用场景
    • 用户的离散交互:例如,按钮的 onClick 事件。用户期望在点击后能“立即”看到反馈,但这个“立即”允许有几十到上百毫秒的延迟。
    • Promisethen:在 Suspense 中,当一个 Promise resolve 后,其后续的渲染任务通常是用户阻塞的。

NormalPriority (普通优先级)

  • Timeout: 5000ms (5s)
  • 策略:任务在 5s 后过期。这是大多数更新的默认优先级,它们不需要立即反馈给用户。
  • 应用场景
    • 网络请求:通过网络获取数据后的状态更新。
    • 非核心的 UI 变化:例如,一个新闻 feed 的加载。
    • useEffect:在 useEffect 中触发的更新通常是普通优先级。

LowPriority (低优先级)

  • Timeout: 10000ms (10s)
  • 策略:任务在 10s 后过期。这些任务可以被显著延迟,只有在更高优先级的任务都完成后才会被执行。
  • 应用场景
    • 数据预取:在用户导航到下一页之前,提前加载数据。
    • 分析事件:发送一些对用户体验无直接影响的分析数据。

IdlePriority (空闲优先级)

  • Timeout: maxSigned31BitInt (一个极大值,约 29 小时)
  • 策略:永不“主动”过期。这类任务只有在浏览器完全空闲时才会被执行。如果此时有更高优先级的任务进来,IdlePriority 的任务会被立即中断。
  • 应用场景
    • 日志记录:记录一些调试信息。
    • 离屏内容的渲染:例如,一个虚拟化列表(Virtual List)中,远在视口之外的列表项的预渲染。

3. 优先级与时间分片的关系

优先级系统与时间分片机制紧密协同:

  1. 任务选择workLoop 总是从 taskQueue 的堆顶取出任务,这保证了拥有最早 expirationTime(即最高优先级)的任务被首先执行。
  2. 中断与恢复:当 workLoop 因为时间片用完而中断时,它并不会忘记当前的任务。在下一个时间片开始时,它会重新检查 taskQueue。如果此时有一个更高优先级的任务(例如,用户点击按钮)被插入到队列中,那么这个新任务会取代之前被中断的任务,优先执行。

案例:假设 React 正在渲染一个低优先级的图表(LowPriority)。渲染进行到一半,时间片用完,workLoop 中断。此时,用户点击了一个按钮(UserBlockingPriority)。

  • scheduleUpdateOnFiber 会创建一个 UserBlockingPriority 的任务,其 expirationTime 会非常早。
  • 这个新任务被 pushtaskQueue 中,并因为其高优先级而“上浮”到堆顶。
  • 在下一个宏任务中,workLoop 重新开始,它 peek taskQueue,发现堆顶已经变成了那个 UserBlockingPriority 的任务。
  • 于是,Scheduler 会转而执行这个按钮点击的更新。只有当这个高优任务完成后,它才会回过头来,继续执行之前被中断的图表渲染任务。

通过这套精密的优先级和调度策略,React 实现了在保证关键交互流畅性的前提下,最大限度地利用计算资源,为现代 Web 应用提供了坚实的性能保障。

Last updated: