Appearance
useTransition
在 React18 中引入了并发渲染模式,以提高应用程序的性能和响应能力。其中,useTransition 是一个重要的 Hook,其将某些状态更新标记为“低优先级”的,从而允许 React 先处理其他更为紧急的任务,等高优先级任务处理完成再来处理当前低优先级的任务,我们把这种渲染效果称作“非阻塞 UI”
例子
js
function SearchInput() {
const [value, setValue] = useState("");
const [isPending, startTransition] = useTransition();
const [list, setList] = useState([]);
const handleSearch = useCallback(
(e) => {
// Input中的更新是高优先级的
setValue(e.target.value);
startTransition(() => {
// 渲染到列表中的更新是低优先级的
setList(Array(1000000).fill(value));
});
},
[value]
);
return (
<div>
<input value={value} onChange={handleSearch} />
<div>
{isPending ? (
<div>Loading...</div>
) : (
list.map((item, index) => <div key={index}>{item}</div>)
)}
</div>
</div>
);
}在上述例子中,我们在输入框中输入内容时,会触发 setValue 函数,将输入框的值更新到 value 状态中。此时,React 会立即更新 UI,将输入框中的内容显示出来。但是其联想的列表内容 list ,是一个大数据量的渲染,会导致页面卡顿,影响用户体验。所以我们需要将其作为一个低优先级的任务来处理,等 Input 中的更新处理完成后再来处理当前低优先级的任务。从而保证页面的流畅性。
用法
jsx
const [isPending, startTransition] = useTransition();- isPending:是一个布尔值,当过渡状态仍在进行中时,其值为 true;否则为 false。
- startTransition:是一个函数,当你希望启动一个新的过渡状态时调用它。
源码解析
对于 useTransition 这个 Hook,其源码位于 react-reconciler/src/ReactFiberHooks.js 文件中。 其也是按照通用 Hook 的方式去实现的。即
- 初次渲染执行
mountTransition函数, 更新渲染阶段执行updateTransition函数。 - hook 也是通过
mountWorkInProgressHook和updateWorkInProgressHook来挂载和更新的。
具体如下
初次渲染阶段
js
function mountTransition(): [
boolean,
(callback: () => void, options?: StartTransitionOptions) => void
] {
// 内部调用 useState 方法去维护 pending 状态
const [isPending, setPending] = mountState(false);
// The `start` method never changes.
// 通过 bind 绑定 startTransition 方法
const start = startTransition.bind(null, setPending);
// 将 start 赋值给 hook.memoizedState, 这样在更新阶段就可以直接使用 start 方法,
// 而不需要每次都重新生成 start 方法,从而提高性能
const hook = mountWorkInProgressHook();
hook.memoizedState = start;
return [isPending, start];
}从源码可以看出, useTransition 内部是通过 useState 来维护 pending 状态的。 核心是 startTransition 方法进行处理的,下面我们看下 startTransition 方法的实现。
js
function startTransition(setPending, callback, options) {
// 获取当前优先级
const previousPriority = getCurrentUpdatePriority();
// 将当前优先级设置为高优先级
setCurrentUpdatePriority(
higherEventPriority(previousPriority, ContinuousEventPriority)
);
// 1. 使用高优先级的任务去 将pending 设置为 true
setPending(true);
// 2. 修改事件优先级上下文,将下面的任务的优先级都改成 transition 优先级,
// 这样就不会打断高优先级的任务
// 具体可以看 requestUpdateLanes 的实现
// 保存当前的 transition
const prevTransition = ReactCurrentBatchConfig.transition;
// 将当前的 transition 设置为 {} 这样 requestUpdateLanes 的时候就进入 transition 流程
ReactCurrentBatchConfig.transition = {};
const currentTransition = ReactCurrentBatchConfig.transition;
try {
// 以 transition 优先级去执行 pending = false
setPending(false);
// 以 transition 优先级去执行 callback
callback();
} finally {
// 恢复之前的事件优先级
setCurrentUpdatePriority(previousPriority);
// 恢复之前的 transition
ReactCurrentBatchConfig.transition = prevTransition;
}
}从上面源码可以看出startTransition 方法主要分为 3 个部分:
- 修改当前事件优先级
currentUpdatePriority, 以高优先级的任务去 将 pending 设置为 true - 将事件优先级上下文修改为 transition 优先级,去执行 pending = false 和 callback
- 恢复之前的事件优先级
这样通过合理的优先级切换,将 startTransition 方法内部的任务都设置为低优先级的任务,并在执行之前将 pending = true;通过高优先级 Update 执行,从而保证了事件的顺序和页面的流畅性。
具体图如下:
更新阶段
js
function updateTransition(): [
boolean,
(callback: () => void, options?: StartTransitionOptions) => void
] {
// 更新阶段将 pending 状态设置为 false
const [isPending] = updateState(false);
// 从缓存中获取 start 方法
const hook = updateWorkInProgressHook();
const start = hook.memoizedState;
return [isPending, start];
}更新阶段就相对简单,通过 hook 对象的缓存去获取 start 方法即可。
注意事项
startTransition的回调函数必须是同步的,而不能是异步的。
js
startTransition(async () => {
await someAsyncFunction();
// ❌ Setting state *after* startTransition call
setPage("/about");
});因为 startTransition 的回调函数是同步执行的,所以不能在 startTransition 的回调函数中执行异步函数。
- 不能用于控制文本输入。因为输入框是需要实时更新的,如果用
useTransition降低了渲染优先级,可能造成输入“卡顿”。
应该是对于文本内容的处理流程可以使用useDeferredValue来优化。
