Appearance
第 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. mountTransition 与 updateTransition: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 的关键步骤解读:
- 优先级切换:
setCurrentUpdatePriority是整个魔法的核心。它将当前更新的优先级临时“降级”到一个较低的水平(TransitionPriority)。 - 划分“赛道”:正因为优先级被降低,当用户回调
callback()中的setState被执行时,requestUpdateLane函数会根据当前的TransitionPriority分配一个TransitionLane。这相当于将这个更新放到了一个“慢车道”上。 - 乐观的
isPending:dispatchOptimisticSetState会立即以高优先级派发一个更新,将isPending状态设置为true。这确保了即使用户的callback执行得很慢,加载中的 UI 也能立刻显示出来,提升了用户的感知体验。 - 执行与恢复:在
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>
);
}工作流程拆解:
- 用户在输入框中输入字符。
handleChange被触发。setInputValue是一个紧急更新,React 会立即开始或继续渲染,以确保输入框的内容得到更新。startTransition被调用。它首先以高优先级将isPending更新为true,然后将当前更新优先级降低。setSearchQuery在低优先级下被调用,React 会为其分配一个TransitionLane,并开始一个可中断的渲染。- 关键点:如果此时用户继续输入,触发了新的
setInputValue紧急更新,React 会立即中断正在进行的setSearchQuery的低优先级渲染,优先处理用户的输入,保证输入框的流畅。当紧急更新完成后,React 会在后台重新开始被中断的Transition渲染。
5. 总结
useTransition 是 React 并发特性的基石。它通过一个简洁的 API,让开发者能够显式地声明更新的优先级,从而解决了UI阻塞的痛点。其内部实现巧妙地结合了 Scheduler 和 Lane 模型,通过临时降低更新优先级,将耗时的渲染任务放入可中断的“过渡”中执行,最终实现了在复杂交互下依然保持界面流畅响应的优异用户体验。