Skip to content

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 也是通过 mountWorkInProgressHookupdateWorkInProgressHook 来挂载和更新的。

具体如下

初次渲染阶段

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 个部分:

  1. 修改当前事件优先级currentUpdatePriority , 以高优先级的任务去 将 pending 设置为 true
  2. 将事件优先级上下文修改为 transition 优先级,去执行 pending = false 和 callback
  3. 恢复之前的事件优先级

这样通过合理的优先级切换,将 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 方法即可。

注意事项

  1. startTransition的回调函数必须是同步的,而不能是异步的。
js
startTransition(async () => {
  await someAsyncFunction();
  // ❌ Setting state *after* startTransition call
  setPage("/about");
});

因为 startTransition 的回调函数是同步执行的,所以不能在 startTransition 的回调函数中执行异步函数。

  1. 不能用于控制文本输入。因为输入框是需要实时更新的,如果用useTransition降低了渲染优先级,可能造成输入“卡顿”。

应该是对于文本内容的处理流程可以使用useDeferredValue来优化。