【React Hooks原理 - useTransition】

概述

在上一篇中我们介绍了useDeferredValue的基本原理,本文主要介绍一下useTransition这个Hook,之所以在这里提到useDeferredValue,是因为这两个Hook都是在React18引入的进行渲染优化的Hooks,在某些功能上是重叠的,主要区别如下:useTransition是在useDeferredValue之前运行,主要是对状态更新更新延迟,即降低setValue更新状态请求的优先级,让其延后更新,来减少不必要的渲染。而useDeferredValue则是通过绑定state来返回一个旧值来延迟新组件的不必要渲染,两者都是降低更新的优先级,只是针对的对象不一致。

基本使用

老规矩,我们还是先定义入手来看看useTransition是如何使用的。

javascript 复制代码
const [isPending, startTransition] = useTransition()

该Hook不接收参数,会返回一个包含两个函数的数组[isPending,startTransition ]

  • isPending:布尔值,告诉你是否正在处理过渡更新,一般可以根据该值添加loading等效果。
  • startTransition :是一个调用一个或多个set 函数更新状态的函数,使用此方法降低状态更新的优先级,进行延迟更新,更新标记为 transition。
javascript 复制代码
import React, { useState, useTransition } from 'react';

function App() {
  const [isPending, startTransition] = useTransition();
  const [inputValue, setInputValue] = useState('');
  const [computedValue, setComputedValue] = useState('');

  const handleChange = (e) => {
    const value = e.target.value;
    setInputValue(value);

    // 使用 startTransition 将计算标记为低优先级
    startTransition(() => {
      const newValue = computeExpensiveValue(value);
      setComputedValue(newValue);
    });
  };

  return (
    <div>
      <input 
        type="text" 
        value={inputValue} 
        onChange={handleChange} 
      />
      {isPending ? <p>Loading...</p> : <p>Computed Value: {computedValue}</p>}
    </div>
  );
}

// 模拟一个耗时计算函数
function computeExpensiveValue(input) {
  let result = '';
  for (let i = 0; i < 100000; i++) {
    result = input;
  }
  return result;
}

export default App;

可能有的人看到这个会有些疑惑,看着写法感觉和useDeferredValue有点类似呀,在用户持续输入耗时任务时起到减少渲染的作用。其实没错,这就是上面说的这两个hook的重叠地方,都是通过降低优先级来处理的,内部一些处理逻辑是一致的,而主要区别在于useDeferredValue是对一个状态值的优化,订阅一个状态值并返回一个旧值,此时已经发起了状态值的更新,组件的重新渲染,只不过被React给挂起了。而useTransition则是直接对更新函数进行操作,不管一个还是多次状态更新主要被其包裹都会推迟更新,此时都不会发起状态更新的请求。

源码解析

Mount阶段

在Mount阶段,主要通过mountTransition来调用startTransition来控制状态的更新。代码如下:由于

javascript 复制代码
function mountTransition(): [
  boolean,
  (callback: () => void, options?: StartTransitionOptions) => void
] {
  const stateHook = mountStateImpl((false: Thenable<boolean> | boolean));
  // The `start` method never changes.
  const start = startTransition.bind(
    null,
    currentlyRenderingFiber,
    stateHook.queue,
    true,
    false
  );
  const hook = mountWorkInProgressHook();
  hook.memoizedState = start;
  return [false, start];
}

从代码能看出mountTransition主要是以下逻辑:

  • 调用mountStateImpl函数来初始化内部状态,即暴露的isPending,用于判断当前是否在执行过渡更新
  • 通过startTransition来管理状态更新,其中会降低优先级和维护isPending状态
  • 通过mountWorkInProgressHook来创建一个新的hook,用于fiber节点对useTransition这个Hook的追踪和管理
  • 返回[isPending, startTransition]数组

可能这里会有疑问,为什么要创建两个Hook:stateHookhook

这是因为hook用于组件fiber来管理useTransition这个Hook的,其中fiber.memonizedState指向的就是useTransition。而stateHook用于管理我们通过startTransition触发的过渡更新,其中通过dispatchSetState维护了isPending的状态,以及降低其优先级为过渡优先级。

javascript 复制代码
// 同步优先级 最高
export const SyncLane = 0b00001;
// 输入框等交互优先级
export const InputContinuousLane = 0b00010;
// 默认优先级
export const DefaultLane = 0b00100;
// useTransition优先级
export const TransitionLane = 0b01000;
// 空闲
export const IdleLane = 0b10000;

如上所示,过渡优先级仅比空闲优先级高,所以将状态更新设置为过渡优先级之后,会优先执行其他任务,最后再执行过渡更新。

下面我们逐一介绍mountStateImplstartTransition这两个函数。

mountStateImpl函数

javascript 复制代码
function mountStateImpl<S>(initialState: (() => S) | S): Hook {
  const hook = mountWorkInProgressHook();
  hook.memoizedState = hook.baseState = initialState;
  const queue: UpdateQueue<S, BasicStateAction<S>> = {
    pending: null,
    lanes: NoLanes,
    dispatch: null,
    lastRenderedReducer: basicStateReducer,
    lastRenderedState: (initialState: any),
  };
  hook.queue = queue;
  return hook;
}

该函数主要就是创建一个Hook并绑定一个初始值initialState,然后根据这个状态值生产一个更新对象queue,最后返回该hook。 此处的initialState就是返回的isPending,默认为false。

当我们通过startTransition来触发过渡更新时,会调用该函数:

其中enableAsyncActions表示是否开启异步操作,较新的React版本会默认开启。为了方便阅读,将该函数拆分为了开启异步和不开启异步两部分来介绍。

开启异步:

javascript 复制代码
function startTransition<S>(
  fiber: Fiber,
  queue: UpdateQueue<S | Thenable<S>, BasicStateAction<S | Thenable<S>>>,
  pendingState: S,
  finishedState: S,
  callback: () => mixed,
  options?: StartTransitionOptions
): void {
  const previousPriority = getCurrentUpdatePriority();
  setCurrentUpdatePriority(
    higherEventPriority(previousPriority, ContinuousEventPriority)
  );

  const prevTransition = ReactSharedInternals.T;
  const currentTransition: BatchConfigTransition = {};

  ReactSharedInternals.T = currentTransition;
  dispatchOptimisticSetState(fiber, false, queue, pendingState);

  try {
    const returnValue = callback();
    const onStartTransitionFinish = ReactSharedInternals.S;
    if (onStartTransitionFinish !== null) {
      onStartTransitionFinish(currentTransition, returnValue);
    }
    if (
      returnValue !== null &&
      typeof returnValue === "object" &&
      typeof returnValue.then === "function"
    ) {
      const thenable = ((returnValue: any): Thenable<mixed>);
      const thenableForFinishedState = chainThenableValue(
        thenable,
        finishedState
      );
      dispatchSetState(fiber, queue, (thenableForFinishedState: any));
    } else {
      dispatchSetState(fiber, queue, finishedState);
    }
  } finally {
    setCurrentUpdatePriority(previousPriority);
    ReactSharedInternals.T = prevTransition;
  }
}
  1. 第一步是通过getCurrentUpdatePriority来获取当前更新任务的优先级,比如持续Input输入则当前优先级为InputContinuousLane

  2. 通过setCurrentUpdatePriority来提高当前任务的优先级higherEventPriority(previousPriority, ContinuousEventPriority),这一步提高优先级是为了保证返回的isPending状态及时更新,不影响用户交互,比如显示loading加载提示,此时还没有进入过渡更新。

  3. ReactSharedInternals保存共享的初始状态,有React内部维护,此处是获取过渡更新状态。

  4. 调用dispatchOptimisticSetState来更新isPending将值设置为true并调用scheduleUpdateOnFiber触发fiber更新

  5. 进入try catch逻辑,这里面就是处理过渡更新,并降低优先级的逻辑。会先执行startTransition包裹的状态更新,并得到返回值returnValue

  6. 判断返回的值是否是Promise对象

    • 如果是,则通过chainThenableValue来等待Promise执行完成并设置其完成状态。然后调用dispatchSetState来降低优先级并等待执行
    • 如果不是则直接调用dispatchSetState来降低优先级并等待执行,在该函数中会调用requestUpdateLane来获取当前优先级,如果是过渡任务,则会返回过渡优先级。
  7. 最后执行setCurrentUpdatePriority恢复之前任务本身的优先级,并更新共享数据ReactSharedInternals的值

Update阶段

主要流程在Mount阶段已经梳理了,在这里看看在Update时内部是怎样运动的。

javascript 复制代码
function updateTransition(): [
  boolean,
  (callback: () => void, options?: StartTransitionOptions) => void,
] {
  const [booleanOrThenable] = updateState(false);
  const hook = updateWorkInProgressHook();
  const start = hook.memoizedState;
  const isPending =
    typeof booleanOrThenable === 'boolean'
      ? booleanOrThenable
      : // This will suspend until the async action scope has finished.
        useThenable(booleanOrThenable);
  return [isPending, start];
}

从代码能看出,主要就是返回最新的isPenging的值,并创建一个更新任务添加到Hook中。

总结

useTransition主要是针对状态更新函数set函数,降低其优先级为过渡优先级,在调度器中延后执行,以达到延迟更新的目的,当用户持续输入时减少渲染。其中主要两点:

  • 内部维护isPending状态,同暴露到应用层,方便进行交互优化(loading提示等)
  • 降低更新优先级为过渡优先级,延迟状态的更新

题外话 - 号外

本文也是根据这些文章学习进行梳理在自己理解的基础上书写的,如有问题,还请指正。

有兴趣的朋友可以关注一下公众号,方便随时随地一起交流学习。

相关推荐
前端小小王15 分钟前
React Hooks
前端·javascript·react.js
迷途小码农零零发25 分钟前
react中使用ResizeObserver来观察元素的size变化
前端·javascript·react.js
娃哈哈哈哈呀1 小时前
vue中的css深度选择器v-deep 配合!important
前端·css·vue.js
旭东怪1 小时前
EasyPoi 使用$fe:模板语法生成Word动态行
java·前端·word
ekskef_sef3 小时前
32岁前端干了8年,是继续做前端开发,还是转其它工作
前端
sunshine6413 小时前
【CSS】实现tag选中对钩样式
前端·css·css3
真滴book理喻4 小时前
Vue(四)
前端·javascript·vue.js
蜜獾云4 小时前
npm淘宝镜像
前端·npm·node.js
dz88i84 小时前
修改npm镜像源
前端·npm·node.js
Jiaberrr4 小时前
解锁 GitBook 的奥秘:从入门到精通之旅
前端·gitbook