likes
comments
collection
share

useTransiton的实现

作者站长头像
站长
· 阅读数 11

本系列是讲述从0开始实现一个react18的基本版本。通过实现一个基本版本,让大家深入了解React内部机制。 由于React源码通过Mono-repo 管理仓库,我们也是用pnpm提供的workspaces来管理我们的代码仓库,打包我们使用rollup进行打包。

仓库地址

本节对应的代码

系列文章:

本章我们通过一个实际的api来讲解react的并发中的运用。在React18中,新增了一个useTransition的api,我们先来看一下这个api的使用。

useTransition的使用

useTransition主要的目的是让我们更新状态,但是不去堵塞UI的渲染。

const [isPending, startTransition] = useTransition()

它不接受任务参数,返回一个数组。包含ispending告诉我们当前的任务的状态。startTransition是一个函数,接受一个函数,用来更新状态,启动更新。 具体的例子和使用可以看官方文档

useTransition的实现

接下来我们从原理和具体的代码分析useTransition是如何实现的。也可以更加的让我们去了解react是如何利用优先级去实现并发的。

useTransition的原理

从之前的章节中,我们晓得react内部通过优先级来区分任务的执行顺序。useTransition通过低优先级的更新策略来实现不堵塞UI的渲染。

useTransition的作用分为2个部分:

  1. 切换UI -> 触发更新
  2. 回调触发更新(低优先级) -> 先显示旧的UI -> 等待新的UI任务加载完成后,再显示新的UI

如下例子中,但我们点击调用selectTab函数时,会触发startTransition函数,传入一个回调函数。

const [isPending, startTransition] = useTransition();
const [tab, setTab] = useState("about");
const [number, setNumber] = useState("about");

function selectTab(nextTab) {
  startTransition(() => {
    setTab(nextTab)
    setNumber(1)
  });
}

整个流程类似大致如图: useTransiton的实现

  1. 当调用selectTab的时候,首先会触发startTransition , 传入一个callback
  2. startTransition执行的时候,react内部调用setPending(true)。然后全局标识currentBatchConfig.transition = 1进入transition的优先级中
  3. 同步调用callback(), 在内部会产生2个任务,由于此时的优先级已经被标记为transition,所以此时的任务也会是transition优先级推入taskQueue中等待调用。
  4. 同时设置setPending(false),这个任务也是transition优先级,等待调度
  5. 重置全局标识,恢复正常的优先级去调度。

useTransition的代码部分 - mount和update

首先我们理解useTransition是一个hook。所以会分为2个mountupdate2个部分。所有的hook逻辑都在fiberHook文件中。

初始化mountTransition

在初始化渲染阶段,当第一次执行useTransition的时候,实际就是执行到mountTransition, 需要返回1个数组。

包含isPending以及startTransition2个部分,用于过度动画和触发低优先级的更新。

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

从上面的代码中我们可以看出,内部调用了一个mountState创建一个isPending变量,用于标识执行的状态。

然后执行mountWorkInProgressHook创建一个新的hook结构。然后将start(startTransition)绑定在memoizedState上。以便于在update的时候使用。

更新updateTransition

当我们点击的时候触发startTransition。实际上是执行的逻辑是怎么样的呢?

我们来看看startTransition的主要逻辑:

function startTransition(setPending: Dispatch<boolean>, callback: () => void) {
  setPending(true); // 1. 触发一个高优先级的更新(同步)点击
  const prevTransition = currentBatchConfig.transition;
  currentBatchConfig.transition = 1;

  callback(); // 2. 触发一个低优先级的更新
  setPending(false);
  currentBatchConfig.transition = prevTransition;
}
  1. 由于我们点击事件的优先级是ImmediatePriority, 所以setPending的任务也是同步优先级执行。
  2. currentBatchConfig.transition = 1后,我们调用callback, 这个时候会触发2个任务。
  3. 每个任务都会去调用requestUpdateLane去绑定有一个优先级。所以我们在currentBatchConfig.transition = 1;的阶段中,返回的都是TransitionLane的优先级。
export function requestUpdateLane() {
  // 增加transition 逻辑
  const isTransition = ReactCurrentBatchConfig.transition !== null;
  if (isTransition) {
    return TransitionLane;
  }

  // 从调度器中获取优先级
  const currentSchedulerPriority = unstable_getCurrentPriorityLevel();
  // scheduler优先级  to lane
  const lane = schedulerPriorityToLane(currentSchedulerPriority);
  return lane;
}
  1. 所以此次执行完后,callback中的任务以及setPending(false)都是TransitionLane的优先级

当更新的时候执行到对应的组件的时候。useTransition实际对应的updateTransition。返回isPending以及下次更新start函数

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

等待高优先级的任务执行完后。开始执行是TransitionLane的优先级的任务,设置ispending = false, 以及对应的callback中的更新任务。

至此,我们的useTransitionhook的逻辑就完全执行完成。