Skip to content

第 4.3 节:useTransition:优先级的动态调整

1. 背景:交互体验的“卡顿”之痛

在复杂的 Web 应用中,一个常见的场景是用户的某个操作会同时触发两种不同类型的更新:

  • 紧急更新:需要立即反馈给用户,例如点击、输入等操作,用户期望界面能瞬间响应。
  • 非紧急更新:计算量较大或可以稍后展示的更新,例如搜索结果的渲染、图表的绘制等。

在 React 18 之前,所有更新的优先级都是相同的。这意味着一个耗时较长的“非紧急更新”会阻塞主线程,导致界面无法响应用户的紧急操作,从而产生“输入框延迟”、“点击按钮无响应”等卡顿现象,严重影响用户体验。

2. useTransition 的诞生:为更新划分优先级

为了解决上述问题,React 18 引入了 useTransition Hook。它允许我们将某些状态更新标记为“过渡(Transition)”,即非紧急更新。这使得 React 可以在渲染这些更新时,被更高优先级的紧急更新中断,从而保证界面的流畅响应。

useTransition 的使用方式非常简洁:

javascript
import { useTransition } from 'react';

const [isPending, startTransition] = useTransition();

它返回一个包含两个元素的数组:

  • isPending:一个布尔值,当 transition 正在进行中时为 true。我们可以用它来向用户展示加载状态。
  • startTransition:一个函数。所有包裹在该函数回调内的状态更新都会被标记为非紧急的 transition 更新。

3. 源码剖析:useTransition 的工作机制

要理解 useTransition 如何动态调整优先级,我们必须深入其在 react-reconciler 包中的实现。

3.1. mountTransitionupdateTransition:Hook 的初始化与更新

与其他 Hook 类似,useTransition 也通过 dispatcher 机制,在组件首次挂载和后续更新时调用不同的函数。

  • mountTransition (ReactFiberHooks.js):在组件首次渲染时调用。
javascript
// packages/react-reconciler/src/ReactFiberHooks.js

function mountTransition(): [
  boolean,
  (callback: () => void, options?: StartTransitionOptions) => void,
] {
  // 1. 创建一个 state hook 来管理 isPending 状态
  const stateHook = mountStateImpl(false);

  // 2. 绑定核心的 startTransition 函数,并预设参数
  const start = startTransition.bind(
    null,
    currentlyRenderingFiber, // 当前 Fiber 节点
    stateHook.queue,         // isPending 状态的更新队列
    true,                    // isPending 的 pending 状态值
    false,                   // isPending 的 finished 状态值
  );

  // 3. 将绑定后的函数存储在 hook 的 memoizedState 中
  const hook = mountWorkInProgressHook();
  hook.memoizedState = start;

  // 4. 返回初始状态和 start 函数
  return [false, start];
}

设计思想mountTransition 的核心任务是“准备”工作。它创建了 isPending 的状态钩子,并将真正的核心逻辑 startTransition 函数与当前组件的上下文(Fiber 节点、状态队列)绑定,然后将这个预备好的函数缓存起来。这确保了 startTransition 在被调用时,能够准确地找到需要更新的组件和状态。

  • updateTransition (ReactFiberHooks.js):在组件更新时调用。
javascript
// packages/react-reconciler/src/ReactFiberHooks.js

function updateTransition(): [
  boolean,
  (callback: () => void, options?: StartTransitionOptions) => void,
] {
  // 1. 获取 isPending 的当前状态
  const [booleanOrThenable] = updateState(false);
  
  // 2. 从 memoizedState 中直接获取缓存的 start 函数
  const hook = updateWorkInProgressHook();
  const start = hook.memoizedState;

  // ... 处理 isPending 的异步情况 ...

  return [isPending, start];
}

设计思想updateTransition 的逻辑非常高效。它直接复用了在 mount 阶段创建并缓存的 start 函数,避免了在每次渲染时都重新创建和绑定函数,这对于性能优化和保持函数引用的稳定性至关重要。

3.2. 核心逻辑 startTransition:优先级的“降级”处理

startTransition 函数是整个 useTransition 机制的核心。它本身不是 Hook,而是由 useTransition 返回的 start 函数在执行时调用的。

javascript
// packages/react-reconciler/src/ReactFiberWorkLoop.js

function startTransition(
  fiber: Fiber,
  queue: UpdateQueue, // isPending 状态的更新队列
  pendingState: boolean, // true
  finishedState: boolean, // false
  callback: () => mixed, // 用户传入的回调函数
  options?: StartTransitionOptions,
): void {
  // 1. 记录并“降级”当前更新的优先级
  const previousPriority = getCurrentUpdatePriority();
  setCurrentUpdatePriority(
    higherEventPriority(previousPriority, ContinuousEventPriority),
  );

  // 2. 创建并设置当前的 Transition 上下文
  const prevTransition = ReactSharedInternals.T;
  const currentTransition: Transition = {};
  ReactSharedInternals.T = currentTransition;

  // 3. 乐观更新:立即以高优先级将 isPending 设置为 true
  dispatchOptimisticSetState(fiber, false, queue, pendingState);

  try {
    // 4. 执行用户回调。此时,内部的所有 setState 都会被标记为 Transition 优先级
    callback();
  } finally {
    // 5. 恢复之前的优先级和 Transition 上下文
    setCurrentUpdatePriority(previousPriority);
    ReactSharedInternals.T = prevTransition;
  }
}

startTransition 的关键步骤解读

  1. 优先级切换setCurrentUpdatePriority 是整个魔法的核心。它将当前更新的优先级临时“降级”到一个较低的水平(TransitionPriority)。
  2. 划分“赛道”:正因为优先级被降低,当用户回调 callback() 中的 setState 被执行时,requestUpdateLane 函数会根据当前的 TransitionPriority 分配一个 TransitionLane。这相当于将这个更新放到了一个“慢车道”上。
  3. 乐观的 isPendingdispatchOptimisticSetState 会立即以高优先级派发一个更新,将 isPending 状态设置为 true。这确保了即使用户的 callback 执行得很慢,加载中的 UI 也能立刻显示出来,提升了用户的感知体验。
  4. 执行与恢复:在 try...finally 结构中,React 执行用户传入的 callback,然后无论成功与否,都将优先级恢复到之前的状态,避免影响后续的更新。

4. 案例分析:流畅的搜索体验

让我们通过一个经典的搜索框案例,来感受 useTransition 的威力。

javascript
function SearchPage() {
  const [inputValue, setInputValue] = useState('');
  const [searchQuery, setSearchQuery] = useState('');
  const [isPending, startTransition] = useTransition();

  const handleChange = (e) => {
    // 紧急更新:输入框的值必须立即响应用户的输入
    // 这个更新会被分配一个高优先级的 Lane (如 InputContinuousLane)
    setInputValue(e.target.value);

    // 非紧急更新:搜索结果的渲染可以稍后进行
    startTransition(() => {
      // 这个更新会被分配一个低优先级的 Lane (TransitionLane)
      setSearchQuery(e.target.value);
    });
  };

  return (
    <div>
      <input value={inputValue} onChange={handleChange} />
      {isPending ? " 正在搜索..." : <SearchResults query={searchQuery} />}
    </div>
  );
}

工作流程拆解

  1. 用户在输入框中输入字符。
  2. handleChange 被触发。setInputValue 是一个紧急更新,React 会立即开始或继续渲染,以确保输入框的内容得到更新。
  3. startTransition 被调用。它首先以高优先级将 isPending 更新为 true,然后将当前更新优先级降低。
  4. setSearchQuery 在低优先级下被调用,React 会为其分配一个 TransitionLane,并开始一个可中断的渲染。
  5. 关键点:如果此时用户继续输入,触发了新的 setInputValue 紧急更新,React 会立即中断正在进行的 setSearchQuery 的低优先级渲染,优先处理用户的输入,保证输入框的流畅。当紧急更新完成后,React 会在后台重新开始被中断的 Transition 渲染。

5. 总结

useTransition 是 React 并发特性的基石。它通过一个简洁的 API,让开发者能够显式地声明更新的优先级,从而解决了UI阻塞的痛点。其内部实现巧妙地结合了 SchedulerLane 模型,通过临时降低更新优先级,将耗时的渲染任务放入可中断的“过渡”中执行,最终实现了在复杂交互下依然保持界面流畅响应的优异用户体验。

Last updated: