Appearance
9.4 useTransition 和 useDeferredValue
在并发模式下,React 提供了两个强大的 Hooks——useTransition 和 useDeferredValue,它们是 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();
}实现主要分为 mountTransition 和 updateTransition,它们都位于 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];
}设计解析:
- 内部状态
isPending:mountTransition内部调用了mountState(false)来创建一个状态isPending,用于追踪过渡的执行状态。这本质上就是一个useState。 startTransition的绑定:startTransition函数是核心。这里通过.bind方法预先传入了当前 Fiber 节点 (currentlyRenderingFiber) 和更新isPending状态的setIsPending函数。当开发者调用startTransition时,React 内部就能知道是哪个组件发起的,并且能够在过渡开始和结束时自动更新isPending状态。- 保存
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;
}
}设计解析:
- 提升优先级执行
isPending:在try块之前,startTransition会将当前更新优先级临时提升,然后立即调用dispatchSetState将isPending设置为true。这确保了isPending的状态变更是高优的,能够被立即渲染,从而让 UI 及时反馈(例如,显示一个 loading 状态)。 - 降级执行
callback:在callback()执行期间,React 的内部上下文(ReactSharedInternals.T)被设置为了一个Transition对象。当callback内部的setState被调用时,requestUpdateLane函数会检查这个上下文,如果发现当前处于一个Transition中,它就会为这次更新分配一个低优先级的TransitionLane。 - 优先级恢复:
finally块确保了在callback执行完毕后,更新优先级和Transition上下文都会被恢复到之前的状态。
2. useDeferredValue:延迟更新值
useDeferredValue 与 useTransition 类似,但它用于延迟一个值的更新。当你有一个值,它的变化会触发开销巨大的渲染,你可以使用 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.js 的 useDeferredValueImpl 中。
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 的本质:
- 内部状态
prevValue:它使用useState来保存上一次的延迟值。 - 使用
useTransition:它直接复用了useTransitionHook 来获取startTransition函数和isPending状态。 useEffect触发过渡:它在useEffect中监听原始value的变化。一旦value改变,它就调用startTransition,并在回调中用新的value来更新内部的prevValue。因为这个更新是在startTransition中完成的,所以它是一个低优先级的过渡更新。- 返回值的选择:
- 当过渡正在进行时(
isPending为true),意味着新的value还在后台渲染中,此时 Hook 返回prevValue,UI 保持不变。 - 当过渡完成后(
isPending为false),意味着后台渲染已完成,此时 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. 总结
useTransition 和 useDeferredValue 是 React 并发渲染的基石,它们为开发者提供了精细控制渲染优先级的工具。
useTransition关注于“动作”,它将一个或多个状态更新包装成一个低优先级的“过渡”,适用于页面跳转、标签页切换等场景。useDeferredValue关注于“值”,它延迟一个值的更新,直到紧急渲染完成,适用于处理高频变化且渲染开销大的数据,如实时搜索建议。
通过这两个 Hooks,React 能够在保证关键交互(如用户输入)流畅响应的同时,在后台高效地处理非紧急的渲染任务,最终实现更优的用户体验。