Skip to content

9.4 useTransition 和 useDeferredValue

在并发模式下,React 提供了两个强大的 Hooks——useTransitionuseDeferredValue,它们是 React 并发特性的核心 API,允许开发者显式地管理和控制状态更新的优先级,从而优化用户体验,尤其是在处理高频更新或重计算开销大的场景中。

1. useTransition:优雅地处理过渡

useTransition Hook 允许我们将某些状态更新标记为“过渡”(Transition),这意味着这些更新可以被更高优先级的更新(如用户输入)中断。这对于防止在数据获取或页面切换等场景下出现界面卡顿至关重要。

设计思想

useTransition 的核心设计思想是分离紧急更新和非紧急更新。用户的输入(如点击、打字)通常被认为是紧急的,需要立即响应。而UI从一个视图过渡到另一个视图的过程,则可以被认为是次要的,可以在后台进行,不阻塞用户交互。

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

  • isPending:一个布尔值,指示过渡是否正在进行中。
  • startTransition:一个函数,它接受一个回调。在该回调中执行的所有状态更新都会被标记为低优先级的过渡更新。

源码分析

useTransition 的入口在 ReactHooks.js 中,它通过 resolveDispatcher 获取当前环境下的调度器,并调用其 useTransition 方法。

javascript
// /Users/xigua/Desktop/FE/react-book/react/packages/react/src/ReactHooks.js

export function useTransition(): [
  boolean,
  (callback: () => void, options?: StartTransitionOptions) => void,
] {
  const dispatcher = resolveDispatcher();
  return dispatcher.useTransition();
}

实现主要分为 mountTransitionupdateTransition,它们都位于 ReactFiberHooks.js 中。

mountTransition

首次渲染时调用 mountTransition

javascript
// /Users/xigua/Desktop/FE/react-book/react/packages/react-reconciler/src/ReactFiberHooks.js

function mountTransition(): [
  boolean,
  (callback: () => void, options?: StartTransitionOptions) => void,
] {
  const [isPending, setIsPending] = mountState(false);
  const start = startTransition.bind(
    null,
    currentlyRenderingFiber,
    setIsPending,
  );
  const hook = mountWorkInProgressHook();
  hook.memoizedState = start;
  return [isPending, start];
}

设计解析:

  1. 内部状态 isPendingmountTransition 内部调用了 mountState(false) 来创建一个状态 isPending,用于追踪过渡的执行状态。这本质上就是一个 useState
  2. startTransition 的绑定startTransition 函数是核心。这里通过 .bind 方法预先传入了当前 Fiber 节点 (currentlyRenderingFiber) 和更新 isPending 状态的 setIsPending 函数。当开发者调用 startTransition 时,React 内部就能知道是哪个组件发起的,并且能够在过渡开始和结束时自动更新 isPending 状态。
  3. 保存 start 函数:最后,将绑定好的 start 函数存入当前 Hook 的 memoizedState 中,以便在后续的更新中复用。

updateTransition

在组件更新时调用 updateTransition

javascript
// /Users/xigua/Desktop/FE/react-book/react/packages/react-reconciler/src/ReactFiberHooks.js

function updateTransition(): [
  boolean,
  (callback: () => void, options?: StartTransitionOptions) => void,
] {
  const [isPending] = updateState(false);
  const hook = updateWorkInProgressHook();
  const start = hook.memoizedState;
  return [isPending, start];
}

设计解析:updateTransition 的逻辑非常简单。它通过 updateState 获取 isPending 的最新值,并从 Hook 的 memoizedState 中直接取出在 mount 阶段已经创建好的 start 函数。这确保了 startTransition 函数的引用在组件的整个生命周期中是稳定的。

startTransition 的核心作用

startTransition 函数(位于 ReactFiberWorkLoop.js)是实现并发更新的关键。

javascript
// /Users/xigua/Desktop/FE/react-book/react/packages/react-reconciler/src/ReactFiberWorkLoop.js

export function startTransition(
  fiber: Fiber,
  queue: UpdateQueue<boolean>,
  pendingState: boolean,
  finishedState: boolean,
  callback: () => void,
  options?: StartTransitionOptions,
) {
  const previousPriority = getCurrentUpdatePriority();
  setCurrentUpdatePriority(
    higherEventPriority(previousPriority, ContinuousEventPriority),
  );

  const prevTransition = ReactSharedInternals.T;
  const currentTransition: Transition = {};
  ReactSharedInternals.T = currentTransition;

  // ... 省略 optimistic update 相关逻辑

  try {
    // 立即将 isPending 设置为 true
    dispatchSetState(fiber, queue, pendingState);
    // 执行回调,其中的 setState 会被标记为低优先级
    callback();
  } finally {
    // ...
    // 渲染完成后,在 commit 阶段,isPending 会被重新设置为 false
    setCurrentUpdatePriority(previousPriority);
    ReactSharedInternals.T = prevTransition;
  }
}

设计解析:

  1. 提升优先级执行 isPending:在 try 块之前,startTransition 会将当前更新优先级临时提升,然后立即调用 dispatchSetStateisPending 设置为 true。这确保了 isPending 的状态变更是高优的,能够被立即渲染,从而让 UI 及时反馈(例如,显示一个 loading 状态)。
  2. 降级执行 callback:在 callback() 执行期间,React 的内部上下文(ReactSharedInternals.T)被设置为了一个 Transition 对象。当 callback 内部的 setState 被调用时,requestUpdateLane 函数会检查这个上下文,如果发现当前处于一个 Transition 中,它就会为这次更新分配一个低优先级的 TransitionLane
  3. 优先级恢复finally 块确保了在 callback 执行完毕后,更新优先级和 Transition 上下文都会被恢复到之前的状态。

2. useDeferredValue:延迟更新值

useDeferredValueuseTransition 类似,但它用于延迟一个值的更新。当你有一个值,它的变化会触发开销巨大的渲染,你可以使用 useDeferredValue 来获取这个值的“延迟”版本。React 会首先使用旧值进行渲染,然后在后台用新值进行一次低优先级的渲染。

设计思想

useDeferredValue 的核心思想是“先响应,后更新”。当一个高频变化的值(例如,搜索框的输入)导致列表重新渲染时,直接使用新值会造成界面卡顿。useDeferredValue 允许 UI 先保持旧值的显示(保证流畅性),同时在后台准备新值的渲染。当新值的渲染完成后,再无缝切换。

源码分析

useDeferredValue 的实现比 useTransition 更为直接,它本质上是 useTransition 的一个语法糖。

javascript
// /Users/xigua/Desktop/FE/react-book/react/packages/react/src/ReactHooks.js

export function useDeferredValue<T>(value: T): T {
  const dispatcher = resolveDispatcher();
  return dispatcher.useDeferredValue(value);
}

其核心实现在 ReactFiberHooks.jsuseDeferredValueImpl 中。

javascript
// /Users/xigua/Desktop/FE/react-book/react/packages/react-reconciler/src/ReactFiberHooks.js

function useDeferredValueImpl<T>(value: T): T {
  const [prevValue, setValue] = useState(value);
  const [isPending, startTransition] = useTransition();

  useEffect(() => {
    startTransition(() => {
      setValue(value);
    });
  }, [value, startTransition]);

  return isPending ? prevValue : value;
}

设计解析: 这段代码非常巧妙地揭示了 useDeferredValue 的本质:

  1. 内部状态 prevValue:它使用 useState 来保存上一次的延迟值。
  2. 使用 useTransition:它直接复用了 useTransition Hook 来获取 startTransition 函数和 isPending 状态。
  3. useEffect 触发过渡:它在 useEffect 中监听原始 value 的变化。一旦 value 改变,它就调用 startTransition,并在回调中用新的 value 来更新内部的 prevValue。因为这个更新是在 startTransition 中完成的,所以它是一个低优先级的过渡更新。
  4. 返回值的选择
    • 当过渡正在进行时(isPendingtrue),意味着新的 value 还在后台渲染中,此时 Hook 返回 prevValue,UI 保持不变。
    • 当过渡完成后(isPendingfalse),意味着后台渲染已完成,此时 Hook 返回最新的 value,UI 更新到最新状态。

在 React 19 的源码中,useDeferredValue 的实现被进一步优化,但其核心思想不变:它在内部追踪值的变化,并使用一个与 useTransition 类似优先级的机制来调度值的更新。

javascript
// /Users/xigua/Desktop/FE/react-book/react/packages/react-reconciler/src/ReactFiberHooks.js (Conceptual)

function updateDeferredValue<T>(value: T): T {
  const hook = updateWorkInProgressHook();
  const prevValue = hook.memoizedState;

  // 检查当前渲染是否在一个 Transition 中,或者是否有更高优先级的更新
  if (isHigherPriorityUpdate()) {
    // 如果是高优更新(如用户输入),则立即返回新值,不延迟
    return value;
  }

  // 否则,返回上一次的值,并调度一个低优先级的更新
  if (notEqual(value, prevValue)) {
    const lane = requestDeferredLane(); // 获取一个延迟渲染的 lane
    scheduleUpdateOnFiber(currentlyRenderingFiber, lane);
  }
  return prevValue;
}

这个概念性的实现更接近底层,它直接与 Lane 模型交互,当检测到值的变化时,它会调度一个低优先级的更新,并暂时返回旧值,从而实现了“延迟”。

3. 总结

useTransitionuseDeferredValue 是 React 并发渲染的基石,它们为开发者提供了精细控制渲染优先级的工具。

  • useTransition 关注于“动作”,它将一个或多个状态更新包装成一个低优先级的“过渡”,适用于页面跳转、标签页切换等场景。
  • useDeferredValue 关注于“值”,它延迟一个值的更新,直到紧急渲染完成,适用于处理高频变化且渲染开销大的数据,如实时搜索建议。

通过这两个 Hooks,React 能够在保证关键交互(如用户输入)流畅响应的同时,在后台高效地处理非紧急的渲染任务,最终实现更优的用户体验。

Last updated: