引言

在浏览器单线程模型中,JavaScript 执行、垃圾回收(GC)、用户交互响应与 UI 渲染竞争同一主线程资源。用户屏幕以 60Hz 刷新率运行时,主线程必须在 16.6ms 内完成每帧渲染任务——一旦 JavaScript 执行超时,将触发渲染帧丢失(Dropped Frames),导致页面卡顿甚至交互冻结。 React 作为数据驱动型 UI 框架,面临核心挑战:

  1. 资源瓶颈:组件树深度遍历(如万级节点 diff)可能阻塞主线程
  2. 优先级冲突:用户交互(点击/输入)需即时响应,数据加载可延迟
  3. 任务竞争:渲染更新与 JS 逻辑共享有限计算资源

为了解决这些问题,React 设计实现了一个调度系统和优先级模型,核心思路是通过时间切片来将长任务拆分成多个小任务执行,避免阻塞渲染帧,此外基于优先级模型对任务根据紧急程度来设置优先级,以便优先处理紧急的任务,如用户输入。

调度系统的实现

抽象接口

React 作为构建用户界面的 JavaScript 库,其协调器(Reconciler)需要处理诸如状态更新、组件挂载/卸载、副作用执行等多样化的任务。这些任务虽然在触发场景和执行逻辑上存在显著差异,但 React 并未为每类任务单独设计调度逻辑,而是将任务抽象成一个特定的 Task 接口,所有需要被调度系统执行的任务都需要符合该接口的规范。

type Task = {
  id: number, // 任务唯一标识
  callback: Callback | null, // 回调
  priorityLevel: PriorityLevel, // 优先级
  startTime: number, // 计划开始时间
  expirationTime: number, // 过期时间
  sortIndex: number, // 在任务队列中排序使用,timerQueue取自startTime,taskQueue取自expirationTime
  isQueued?: boolean, // 是否已经在任务队列中
};

我们称实现 Task 接口的 js 对象为 task,task 是调度系统的最小调度单元。从 Task 可以看出,每个 task 都有一个 id、callback、priorityLevel、startTime 等字段,每个字段都有各自的作用,例如调度系统执行 task 最终执行的其实是 task.callback, priorityLevel 记录了该 task 的优先级。

双任务队列设计与最小堆

优先级队列

现代前端框架的调度系统需要统筹异步任务的优先级与执行时机。传统单队列(FIFO)结构无法满足差异化调度需求,因此主流方案采用优先级队列 (Priority Queue)实现任务排序。React 调度器在此基础上创新性地引入双队列架构 ,通过两个基于小顶堆(Min-Heap)实现的优先级队列协同工作:

  • taskQueue (立即执行队列):存储已就绪的高优任务(如用户交互),基于过期时间 (expirationTime)排序。过期时间越早(数值越小),优先级越高,队列顶部始终是当前最紧急任务
  • timerQueue (延迟执行队列):存储尚未就绪的低优任务(如数据预加载),基于计划时间 (startTime)排序。计划时间越早(数值越小),触发调度的时机越快

当任务被创建时,React会根据 task.startTime 来决定存储到哪个队列中:

  • task.startTime>currentTime,低优先任务,存储到 timerQueue 延后执行。
  • task.startTime<=currentTime,高优先级任务,存储到 taskQueue 尽快执行。

React 调度系统只会执行 taskQueue 中的任务,并且会不时地检查 timerQueue 队列,如果有 task.startTime<=currentTime 的任务,会被提升到 taskQueue 队列中。

function advanceTimers(currentTime: number) {
  // 检查timerQueue是否有过期任务,并将其移动到taskQueue中
  let timer = peek(timerQueue);
  while (timer !== null) {
    if (timer.callback === null) {
      // 任务被取消,出队
      pop(timerQueue);
    } else if (timer.startTime <= currentTime) {
      // 任务过期,移动到taskQueue中尽快执行
      pop(timerQueue);
      timer.sortIndex = timer.expirationTime;
      push(taskQueue, timer);
      if (enableProfiling) {
        markTaskStart(timer, currentTime);
        timer.isQueued = true;
      }
    } else {
      return;
    }
    timer = peek(timerQueue);
  }
}

advanceTimers 专门负责检查 timerQueue 队列是否有过期的任务,如果有则移动到 taskQueue 中。

需要注意的是,虽然 timerQueue 和 taskQueue 我们称为队列,但是本质上两者都是以最小堆的形式存在。 peek 函数返回的是最小堆的堆顶,timerQueue 和 taskQueue 都采用 sortIndex 字段来决定元素在堆中的位置,但是两者 sortIndex 字段的取值不同,timerQueue 取自 startTime,taskQueue 取自 expirationTime。

timerQueue 越前面的元素 startTime 越小,需要尽早执行,taskQueue 越前面的元素 expirationTime 越小,越需要尽快执行。

在 advanceTimers 函数中不断取出(不出堆)堆顶,每次遇到过期任务就从堆中 pop 堆顶并 push 到 taskQueue 中,不断重复此操作,直到堆为空或者堆顶计划开始时间大于当前时间。 如果堆顶计划开始时间大于当前时间,由于最小堆堆顶最小的特性,后面的元素的计划开始时间都必然大于堆顶元素的计划开始时间,也就是说后面的元素也必然没有过期,因此直接 return 退出函数。

最小堆

堆(Heap) 是一种特殊的树形数据结构,通常是一个 完全二叉树(Complete Binary Tree) ,它满足以下性质:

  • 父节点与子节点之间满足堆序关系
    • 最大堆(Max Heap) 中,父节点的值总是大于或等于其子节点的值。
    • 最小堆(Min Heap) 中,父节点的值总是小于或等于其子节点的值。 下面是一个最小堆的例子
    1
   / \
  2   3
 / \
4   5

虽然堆是树形结构,但是实际使用时也可以用层级排序的数组表示,此时堆具有完全二叉树的规律:

节点索引
根节点0
左子节点i*2 + 1
右子节点i*2 + 2
父节点floor((i-1)/2) 或 (i-1) >>> 1

在完全二叉树中,除了最后一层外每个节点都有两个子节点,那么第 k (从 0 开始)层的节点数就是 2^k - 1,第 k (从 0 开始)层第 m (从 0 开始)个节点索引 i 就是 2^k - 1 + m,那么其左子节点在第 k+1 层第 2 m 个节点,那么就是:

2^(k+1) - 1 + 2m = 2(2^k + m) - 1 = 2(i + 1) - 1 = 2i + 1

右子节点只要在左子节点基础上加 1 就行了是,即 2i + 2。同时也可以反推会父节点下标 i,推导过程略,有兴趣可以自行查询。 堆有三个重要的操作:

  • peek:返回堆顶
  • Insert/push:插入元素
  • pop:取出堆顶
export function peek<T: Node>(heap: Heap<T>): T | null {
  return heap.length === 0 ? null : heap[0];
}

peek 操作时间复杂度是 O (1),而插入元素为了保持堆序需要执行上浮,同理,取出堆顶后为了保持堆序需要执行下沉。 所谓的**上浮是指元素插入到末尾后,通过不断与父节点比较交换,从而保持原有堆序的效果的操作。 **

export function push<T: Node>(heap: Heap<T>, node: T): void {
  const index = heap.length;
  heap.push(node);
  siftUp(heap, node, index);
}
function siftUp<T: Node>(heap: Heap<T>, node: T, i: number): void {
  let index = i;
  while (index > 0) {
    const parentIndex = (index - 1) >>> 1;
    const parent = heap[parentIndex];
    if (compare(parent, node) > 0) {
      // The parent is larger. Swap positions.
      heap[parentIndex] = node;
      heap[index] = parent;
      index = parentIndex;
    } else {
      // The parent is smaller. Exit.
      return;
    }
  }
}

下沉则是指在堆弹出堆顶后,将堆最后一个元素移动到队头位置,此时此元素作为堆顶,然后不断将其与左右子节点比较交换,最后保持原有堆序的操作。

export function pop<T: Node>(heap: Heap<T>): T | null {
  if (heap.length === 0) {
    return null;
  }
  const first = heap[0];
  const last = heap.pop();
  if (last !== first) {
    heap[0] = last;
    siftDown(heap, last, 0);
  }
  return first;
}
 
function siftDown<T: Node>(heap: Heap<T>, node: T, i: number): void {
  let index = i;
  const length = heap.length;
  const halfLength = length >>> 1;
  while (index < halfLength) {
    const leftIndex = (index + 1) * 2 - 1;
    const left = heap[leftIndex];
    const rightIndex = leftIndex + 1;
    const right = heap[rightIndex];
 
    // If the left or right node is smaller, swap with the smaller of those.
    if (compare(left, node) < 0) {
      if (rightIndex < length && compare(right, left) < 0) {
        heap[index] = right;
        heap[rightIndex] = node;
        index = rightIndex;
      } else {
        heap[index] = left;
        heap[leftIndex] = node;
        index = leftIndex;
      }
    } else if (rightIndex < length && compare(right, node) < 0) {
      heap[index] = right;
      heap[rightIndex] = node;
      index = rightIndex;
    } else {
      // Neither child is smaller. Exit.
      return;
    }
  }
}

上浮和下沉两者最坏情况下的时间复杂度都是 O (logn),这使得堆的插入和弹出都非常高效,适用于需要频繁插入的场景。 如果采用传统队列(FIFO)+ 二分搜索的方式实现任务队列,二分搜索的时间复杂度是 O(logn),但是每次插入都会导致插入位置后面所有的元素移动后一位,这使得时间复杂度会退化至 O (n)。同理,每次弹出队头元素都会使后面的元素向前移动一位,时间复杂度也是 O (n),而最小堆上浮和下沉都是通过交换节点实现的,并且上浮或下沉在最坏的情况下也只要进行 logn 次交换,因此每次平均时间复杂度是 O (logn),性能更好,更适合这种频繁插入、弹出的操作。

操作最小堆传统排序队列(FIFO)
取最小值O (1)O (1)
弹出最小值O (logn)O (n)
插入元素O (logn)O (n)
空间复杂度O (n)O (n)

执行的时机

调度任务会临时存储在双队列中,最终总归是要执行的,而执行的时机非常重要。我们知道浏览器运行时底层是由一个事件循环机制实现异步的,异步任务也分为微任务和宏任务,微任务优先级更高,每个事件循环周期内都会执行完所有微任务,而宏任务优先级较低,一个事件循环周期只执行一个宏任务,,而 React 调度任务主要是在宏任务中执行的。

function unstable_scheduleCallback(
  priorityLevel: PriorityLevel,
  callback: Callback,
  options?: {delay: number},
): Task {
  var currentTime = getCurrentTime();
 
  var startTime;
  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;
  switch (priorityLevel) {
    case ImmediatePriority:
      // Times out immediately
      timeout = -1;
      break;
    case UserBlockingPriority:
      // Eventually times out
      timeout = userBlockingPriorityTimeout;
      break;
    case IdlePriority:
      // Never times out
      timeout = maxSigned31BitInt;
      break;
    case LowPriority:
      // Eventually times out
      timeout = lowPriorityTimeout;
      break;
    case NormalPriority:
    default:
      // Eventually times out
      timeout = normalPriorityTimeout;
      break;
  }
 
  var expirationTime = startTime + timeout;
 
  var newTask: Task = {
    id: taskIdCounter++,
    callback,
    priorityLevel,
    startTime,
    expirationTime,
    sortIndex: -1,
  };
  if (enableProfiling) {
    newTask.isQueued = false;
  }
 
  if (startTime > currentTime) {
    // This is a delayed task.
    newTask.sortIndex = startTime;
    push(timerQueue, newTask);
    if (peek(taskQueue) === null && newTask === peek(timerQueue)) {
      // All tasks are delayed, and this is the task with the earliest delay.
      if (isHostTimeoutScheduled) {
        // Cancel an existing timeout.
        cancelHostTimeout();
      } else {
        isHostTimeoutScheduled = true;
      }
      // Schedule a timeout.
      requestHostTimeout(handleTimeout, startTime - currentTime);
    }
  } else {
    newTask.sortIndex = expirationTime;
    push(taskQueue, newTask);
    if (enableProfiling) {
      markTaskStart(newTask, currentTime);
      newTask.isQueued = true;
    }
    // Schedule a host callback, if needed. If we're already performing work,
    // wait until the next time we yield.
    if (!isHostCallbackScheduled && !isPerformingWork) {
      isHostCallbackScheduled = true;
      requestHostCallback();
    }
  }
 
  return newTask;
}

Unstable_scheduleCallback 函数是 React 暴露给外层使用的创建调度任务的 API,正如我们前面所说的,Unstable_scheduleCallback 函数会根据优先级(priorityLevel)来确定计划开始时间(startTime),并通过判断计划开始时间来决定将任务存储在 timerQueue 还是 taskQueue 中,并且会执行不同的操作。

graph TD
    A[创建新任务] --> B{startTime > currentTime?}
    B -->|是| C[加入timerQueue]
    B -->|否| D[加入taskQueue]
    C --> E[设置hostTimeout]
    D --> F[requestHostCallback]
    E -->|超时| G[advanceTimers]
    G --> H[迁移到taskQueue]
    H --> F

根据前面的代码分析,任务执行的关键在于 requestHostCallback 函数和 requestHostTimeout 函数。 首先来看 requestHostTimeouthandleTimeoutrequestHostTimeout 函数本质上是对 setTimeout 的封装。

// 捕获对本地API的本地引用,以防polyfill覆盖它
const localSetTimeout = typeof setTimeout === 'function' ? setTimeout : null;
function requestHostTimeout(
  callback: (currentTime: number) => void,
  ms: number,
) {
 // 保存定时器id,以便后面可以取消时可以使用
  taskTimeoutID = localSetTimeout(() => {
    callback(getCurrentTime());
  }, ms);
}

关键在于 handleTimeout 函数,定时器到期时会执行此函数。

function handleTimeout(currentTime: number) {
  isHostTimeoutScheduled = false; // 标记当前没有定时器执行调度任务
  advanceTimers(currentTime); // 检查timerQueue是否有过期任务
 
  if (!isHostCallbackScheduled) { // 如果任务队列没有被调度执行
    if (peek(taskQueue) !== null) { // taskQueue不为空
       // 执行调度,同时标记
      isHostCallbackScheduled = true;
      requestHostCallback();
    } else {
      // 如果taskQueue是空的,则直接取timerQueue堆顶调度定时器延迟执行
      const firstTimer = peek(timerQueue);
      if (firstTimer !== null) {
        requestHostTimeout(handleTimeout, firstTimer.startTime - currentTime);
      }
    }
  }
}

handleTimeout 函数中可以看出,其最终执行的也是 requestHostCallback 函数,所以前面本质上只是使用了定时器延迟执行了 requestHostCallback 函数。接下来,我们重点关注 requestHostCallback 函数。

function requestHostCallback() {
  if (!isMessageLoopRunning) {
    isMessageLoopRunning = true;
    schedulePerformWorkUntilDeadline();
  }
}

requestHostCallback 函数很短,核心逻辑只有 isMessageLoopRunning 标志和 schedulePerformWorkUntilDeadline 函数调用。

let schedulePerformWorkUntilDeadline;
if (typeof localSetImmediate === 'function') {
  schedulePerformWorkUntilDeadline = () => {
    localSetImmediate(performWorkUntilDeadline);
  };
} else if (typeof MessageChannel !== 'undefined') {
  const channel = new MessageChannel();
  const port = channel.port2;
  channel.port1.onmessage = performWorkUntilDeadline;
  schedulePerformWorkUntilDeadline = () => {
    port.postMessage(null);
  };
} else {
  schedulePerformWorkUntilDeadline = () => {
    localSetTimeout(performWorkUntilDeadline, 0);
  };
}

schedulePerformWorkUntilDeadline 和前面的 localSetTimeout 函数一样,都是对原生 API 的封装,SchedulePerformWorkUntilDeadline 会根据不同的环境采用不同的 API:

  1. Node 或者 IE 浏览器环境下,使用 setImmediate 函数。
  2. DOM 或者 Worker 环境下,使用 MessageChannel
  3. 使用 setTimeout 进行兜底,因为 setTimeout 在绝大多数环境和旧浏览器中都具有良好的兼容性。

React 在 DOM 环境下优先使用 MessageChannel 而不是直接使用 setTimeout,这是因为在 HTML 规范中,嵌套 5 次以上的 setTimeout(..., 0) 强制 4ms 最小延迟,并且相较于 setTimeout,MessageChannel 不仅同样具有良好的兼容性,而且延迟更低。

Timers can be nested; after five such nested timers, however, the interval is forced to be at least four milliseconds.

时间切片

React 创新性地引入了时间切片特性,所谓的时间切片是指将长任务拆分成多个小任务间隔性地执行,从而避免长时间地阻塞主线程。时间切片是一种非常有效的优化长任务的方式。由于微任务和同步任务具有阻塞性,会占用主线程阻塞 UI 渲染,因此可以看到 React 采用 MessageChannel 和 setTimeout 宏任务来避免阻塞。

注意:Nodejs 中的事件循环机制和浏览器中的有所不同,因此setImmediate 不能以宏任务/微任务的视角来看待,但是使用方式如何,其目的都是为了避免阻塞。

performWorkUntilDeadline 是时间切片的入口函数,从 schedulePerformWorkUntilDeadline 函数中可以看出, schedulePerformWorkUntilDeadline 针对不同环境采用对应的异步 API 来调用 performWorkUntilDeadline 函数,接下来,我们继续关注 performWorkUntilDeadline

const performWorkUntilDeadline = () => {
  if (enableRequestPaint) {
    needsPaint = false;
  }
  if (isMessageLoopRunning) {
    const currentTime = getCurrentTime();
    // Keep track of the start time so we can measure how long the main thread
    // has been blocked.
    startTime = currentTime;
 
    // If a scheduler task throws, exit the current browser task so the
    // error can be observed.
    //
    // Intentionally not using a try-catch, since that makes some debugging
    // techniques harder. Instead, if `flushWork` errors, then `hasMoreWork` will
    // remain true, and we'll continue the work loop.
    let hasMoreWork = true;
    try {
      hasMoreWork = flushWork(currentTime);
    } finally {
      if (hasMoreWork) {
        // If there's more work, schedule the next message event at the end
        // of the preceding one.
        schedulePerformWorkUntilDeadline();
      } else {
        isMessageLoopRunning = false;
      }
    }
  }
};

performWorkUntilDeadline 函数通过调用 flushWork 函数去执行调度任务,这里有一个重要的逻辑就是 hasMoreWork,它标志着任务队列是否执行完毕,如果没有执行完,那么就继续再调用 schedulePerformWorkUntilDeadline 函数去异步调用自身。

function flushWork(initialTime: number) {
  // ... 省略部分代码
  // We'll need a host callback the next time work is scheduled.
  isHostCallbackScheduled = false;
  if (isHostTimeoutScheduled) {
    // We scheduled a timeout but it's no longer needed. Cancel it.
    isHostTimeoutScheduled = false;
    cancelHostTimeout();
  }
 
  isPerformingWork = true;
  const previousPriorityLevel = currentPriorityLevel;
  try {
      return workLoop(initialTime);
    }
  } finally {
    currentTask = null;
    currentPriorityLevel = previousPriorityLevel;
    isPerformingWork = false;
  }
}

flushWork 下一步调用 workLoop 函数,这里需要注意在 isHostCallbackScheduledisPerformingWork 两个标志的设置。

function workLoop(initialTime: number) {
  let currentTime = initialTime;
  advanceTimers(currentTime);
  currentTask = peek(taskQueue);
  while (currentTask !== null) {
    if (!enableAlwaysYieldScheduler) {
      if (currentTask.expirationTime > currentTime && shouldYieldToHost()) {
        // This currentTask hasn't expired, and we've reached the deadline.
        break;
      }
    }
    // $FlowFixMe[incompatible-use] found when upgrading Flow
    const callback = currentTask.callback;
    if (typeof callback === 'function') {
      // $FlowFixMe[incompatible-use] found when upgrading Flow
      currentTask.callback = null;
      // $FlowFixMe[incompatible-use] found when upgrading Flow
      currentPriorityLevel = currentTask.priorityLevel;
      // $FlowFixMe[incompatible-use] found when upgrading Flow
      const didUserCallbackTimeout = currentTask.expirationTime <= currentTime;
      if (enableProfiling) {
        // $FlowFixMe[incompatible-call] found when upgrading Flow
        markTaskRun(currentTask, currentTime);
      }
      const continuationCallback = callback(didUserCallbackTimeout);
      currentTime = getCurrentTime();
      if (typeof continuationCallback === 'function') {
        // If a continuation is returned, immediately yield to the main thread
        // regardless of how much time is left in the current time slice.
        // $FlowFixMe[incompatible-use] found when upgrading Flow
        currentTask.callback = continuationCallback;
        if (enableProfiling) {
          // $FlowFixMe[incompatible-call] found when upgrading Flow
          markTaskYield(currentTask, currentTime);
        }
        advanceTimers(currentTime);
        return true;
      } else {
        if (enableProfiling) {
          // $FlowFixMe[incompatible-call] found when upgrading Flow
          markTaskCompleted(currentTask, currentTime);
          // $FlowFixMe[incompatible-use] found when upgrading Flow
          currentTask.isQueued = false;
        }
        if (currentTask === peek(taskQueue)) {
          pop(taskQueue);
        }
        advanceTimers(currentTime);
      }
    } else {
      pop(taskQueue);
    }
    currentTask = peek(taskQueue);
    if (enableAlwaysYieldScheduler) {
      if (currentTask === null || currentTask.expirationTime > currentTime) {
        // This currentTask hasn't expired we yield to the browser task.
        break;
      }
    }
  }
  // Return whether there's additional work
  if (currentTask !== null) {
    return true;
  } else {
    const firstTimer = peek(timerQueue);
    if (firstTimer !== null) {
      requestHostTimeout(handleTimeout, firstTimer.startTime - currentTime);
    }
    return false;
  }
}

从代码中可以看出 workLoop 就是 React 任务调度执行的最终执行函数了,它最终执行的是 task.callback。通过 while 不断取出 taskQueue 堆顶任务执行。

while 循环中有一个中断分支,其中的 enableAlwaysYieldScheduler 是用于测试的,生产环境通常为false。currentTask.expirationTime > currentTime 则是判断当前任务是否未过期,如果当前任务未过期,那么就理解中断循环。

中断的另一个因素是 shouldYieldToHost 函数, shouldYieldToHost 函数主要是用于判断执行时间是否过长(即产生长任务),shouldYieldToHost 函数是实现时间切片的关键。

export const frameYieldMs = 5;
 
let frameInterval = frameYieldMs;
let startTime = -1;
 
function shouldYieldToHost(): boolean {
  // ... 省略部分代码
  const timeElapsed = getCurrentTime() - startTime;
  if (timeElapsed < frameInterval) {
    // The main thread has only been blocked for a really short amount of time;
    // smaller than a single frame. Don't yield yet.
    return false;
  }
  // Yield now.
  return true;
}

可以看到,shouldYieldToHost 函数的作用是比较 getCurrentTime () - startTime,判断当前时间距离开始时间是否超过了 frameInterval,也就是 5 ms,如果超过了这个时间 shouldYieldToHost 就会返回 true,从而中断 workLoop 函数中的循环,即中断任务队列的执行。

while 循环结束后,React 会检查是否存在未完成的任务,如果有就返回 trueperformWorkUntilDeadline 将其存储为 hasMoreWork,并重新执行 schedulePerformWorkUntilDeadline,从而再次异步调用 workLoop,此时第一个未被完成的任务就是 taskQueue 的堆顶,会被重新执行,这就实现了任务执行的恢复。通过这样一个巧妙地设计,React 实现了时间切片来改善长任务的阻塞问题。

sequenceDiagram
    Scheduler->>+Browser: 请求调度 (requestHostCallback)
    Browser->>+Scheduler: 执行任务 (performWorkUntilDeadline)
    Scheduler->>Scheduler: workLoop(hasTimeRemaining, initialTime)
    Scheduler->>Task: 执行任务单元 (performTask)
    Scheduler->>Browser: 检查剩余时间 (shouldYield)
    alt 时间充足
        Scheduler->>Task: 继续执行
    else 时间不足
        Scheduler->>Browser: 暂停并请求后续调度
    end

优先级模型

graph TD
    A[ImmediatePriority] -->|最高| B[UserBlockingPriority]
    B --> C[NormalPriority]
    C --> D[LowPriority]
    D -->|最低| E[IdlePriority]

React 调度系统中的五个 priority 主要对应“用户体验上的紧急程度”:

  • ImmediatePriority(最高)
    • 语义:必须立刻执行(几乎不允许延迟)
    • 特点:timeout = -1,等价于“已过期”,会强力抢占
    • 场景:较少直接使用;更像内部兜底/紧急同步
  • UserBlockingPriority
    • 语义:用户正在等(输入、点击、滚动反馈)
    • 特点:超时较短(“可以切片,但不能拖太久”)
    • 场景:受控输入、点击触发的状态更新、滚动/拖拽相关更新
  • NormalPriority
    • 语义:普通更新(用户不一定立刻感知,但也不能无限拖)
    • 特点:默认优先级
    • 场景:大多数 setState、网络请求回来的 UI 刷新
  • LowPriority
    • 语义:可以晚点再做(不影响当前交互)
    • 特点:超时更长
    • 场景:非关键列表刷新、预加载后的渲染、后台刷新
  • IdlePriority(最低)
    • 语义:只有“系统空闲”才做
    • 特点:超时设置为极大值
    • 场景:预计算、缓存整理、低频统计类工作