第五章: useTransition源码解析

424 阅读6分钟

前言

这章继续讲可以触发组件更新的Hook,useTransition是18.0.0引入的Hook,useTransition 是一个帮助你在不阻塞 UI 的情况下更新状态的,这个是与useStateuseReducer区别的地方,react中Fiber 架构通过将工作分为多个优先级来实现可中断性。
react 任务被划分为不同的优先级,包括同步任务(高优先级)、异步任务(中优先级)和空闲时执行的任务(低优先级)。这种优先级划分允许 React 在工作循环中根据任务的优先级动态调整执行顺序。
useTransition通过降低一部分的优先级,不阻塞页面ui更新。

源码讲解

const [isPending, startTransition] = useTransition()
startTransition(() => { 
});

mount阶段

准备阶段和useState前行一致,useTransition调用的是HooksDispatcherOnMountInDEV.useTransition方法,在调用check方法检查;
核心代码调用mountTransition方法,通过mountState(false)初始化状态, 代码参考useState,这里简单讲解一下,就是创建hook对象和queue对象,将hook挂载到fiber.memoizedState属性上;
创建startTransition的hook,在调用mountWorkInProgressHook方法,创建一个hook对象,将hook挂载到mountState(false)创建的hook.next后;

const hook = { 
    memoizedState: startTransition.bind(null, dispach), 
    baseState: null, 
    baseQueue: null,  // 这个和useState有些区别
    queue: null, 
    next: null 
}

唯一的区别就是memoizedState是react内部的方法,而不是传入的属性;
返回结果 [mountState(false)[0],startTransition.bind(null, dispach)]

以下总结:

  • useTransition是基于useStatemountState方法,创建一个初始化为false的state,
  • 和创建一个startTransition方法的hook对象,
  • 返回一个数组长度为2的数据,第一项为state和第二项startTransition方法;

调用startTransition

首先调用dispatch(true)方法,代码参考setState,主要是创建update链表和调用更新方法; 然后调用dispatch(false)方法,这里调用dispath方法和上面的方法不是一样的效果,在调第二个dispatch方法之前,会往ReactSharedInternals.ReactCurrentBatchConfig.transition._updatedFibers = new Set()上创建一个set,这个可以理解为是isTransition标识,将set.add(fiber),然后走setSate操作;

const update = {
    lane: 64, //目前可以理解成这个是transition的标识,二进制,
    action: false,
    hasEagerState: false,
    eagerState: null,
    next: null
};

在讲useState的章节时,lane是和react lane模型息息相关,是和你调用的事件有关(比如我用的事件是通过click方法内触发startTransition,这个lane就是1),之前让先忽略,目前先理解dispatch(true)创建的update.lane优先于dispatch(false)的update.lane;
把update变成环状链表,queue.pending = update,这样就会在update链表中有一个lane为64,值为false的update和一个lane为1,值为true的update,所以startTransition的callback内部的lane都是64;
清空 ReactSharedInternals.ReactCurrentBatchConfig.transition._updatedFibers ,后面在执行非transition就不会是lane为64了;

以下总结:

  • 调用dispatch(true)和dispatch(false)两个方法,然后在调用startTransition的callback;
  • 两个dispatch就创建不同的lane,update链表两个不同的lane;
  • startTransition的callback就不会和外部的lane是同一个;
  • 执行callback函数;

update阶段

useTransition调用的是HooksDispatcherOnUpdateInDEV.useTransition方法,显示调用check方法,核心调用的是updateTransition,先调用updateState方法;
遍历update链表,这里和setState一致,在讲useState没有关注渲染renderLanes,如果当前的update.lane属于renderLanes子集,那么就处理当前的update,否则就先不处理;
第一次的renderLanes是1,是个第一次的dispacth方法的lane一致,和第二个dispacth的lane不一致,先不处理,掠过,执行结果

hook.memoizedState = true;
hook.baseQueue = true;
hook.baseQueue = update;
queue.lastRenderedState = true;

这里和useState一样,唯一区别的是 hook.baseQueue = update,将未执行的update保存到baseQueue对象上,clone startTransition的hook,上一个hook的next指向当前的hook,返回 [true,startTransition.bind(null, dispach)];
react在lane模型中发现还有未执行的任务,就是第二个dispatch方法的update队列,继续重新更新,在更新的执行 useTransition 方法,唯一的区别是当前任务队列是放在 hook.baseQueue ,而非hook.queue.pending属性上,但是没关系,执行update任务也是放在hook.baseQueue上,重新赋值:

queue.lastRenderedState = false; 
hook.memoizedState = false; 
hook.baseQueue = null;
hook.baseState = false;

以下总结:

  • useTransition代码是基于useState部分实现的;
  • useTransition会触发两次更新,callback内部的优先级会很靠后;
  • useTransition不像useState或者useReducer那样单独使用,通常都是配合其他的方法完整特定场景;

image.png

这解决了什么问题?

构建流畅且反应快速的页面是十分不容易的,有时很细微的操作,可能会有很繁琐的任务需要更新,因为js是单线程的,在更新的时候可能会阻塞用户的操作,这样网站就会显得十分卡顿;
为了更好的优化,将一些重要的、紧急的操作可以优先更新,一些非重要的,不会立即更新,包含的更新startTransition被视为非紧急更新,如果出现更紧急的更新(例如点击或按键),则会被中断。如果用户中断转换(例如,连续输入多个字符),React 将抛出异常未完成的陈旧渲染工作,仅渲染最新更新。
startTransition可以让您保持大多数交互的敏捷性,即使它们会导致重大的 UI 更改。它们还可以让您避免浪费时间呈现不再相关的内容。

与其他方案对比

从源码中发现,startTransition会触发两次更新,一先一后两次更新,如果没有这个api可以通过setTimeout模拟实现这种效果吗?

setPending(true);
setTimeout(()=>{
    setPending(false);
    // other deal
},timer);

这样也能触发两次更新,这样把setTime回调模拟startTransition的回调,看起来是没什么问题,深究一下原理:
setTimeout回调函数内的更新渲染阶段,渲染阶段仍然会阻塞,所以之前的方案都是节流/防抖方法减少不必要渲染;
主要区别是,setTimeout的渲染仍然是legacy 模式,legacy 模式渲染执行期间是没有可打断的时机的,而startTransition的回调渲染模式是concurrent,就在workLoop阶段(协调阶段),js会释放线程,这样会有时机可以操作页面,具体legacy和concurrent的逻辑在后面的文章中更新,这里就不细讲了;
通常我们都会给timer设定一个时间,这个时间长短都不太合适,根据具体场景设置一个可以相对接受的值,而startTransition就不会有这个问题,startTransition会在优先级高的完成之后执行;
补充一点,startTransition的回调会立即执行,而是降低其中的优先级使其延迟更新;

关于一些特别的用法

// 使用方法1
startTransition(() => {
    setState("A");
    setState("B");
});

// 使用方法2
startTransition(() => {
    setState("A")
});
startTransition(() => {
    setState("B")
});

以上两种方法,在使用中没有区别,A和B会批次更新,即使不放在同一个startTransition内,多个startTransition也会批次统一处理;

startTransition(() => {
    setState("A");
    startTransition(()=>{
        setState("B")
    })
});

即使有嵌套关系,A和B也会批次更新,唯一的区别就是外层的isPending = true是立即执行,而内层的isPending = true 和 isPending = false是同时执行的,内层的useTransition不会有中间过渡态;

参考文档

New feature: startTransition