概述
在上一篇中我们介绍了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:stateHook
、hook
?
这是因为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;
如上所示,过渡优先级仅比空闲优先级高,所以将状态更新设置为过渡优先级之后,会优先执行其他任务,最后再执行过渡更新。
下面我们逐一介绍mountStateImpl
、startTransition
这两个函数。
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;
}
}
-
第一步是通过
getCurrentUpdatePriority
来获取当前更新任务的优先级,比如持续Input输入则当前优先级为InputContinuousLane
-
通过
setCurrentUpdatePriority
来提高当前任务的优先级higherEventPriority(previousPriority, ContinuousEventPriority)
,这一步提高优先级是为了保证返回的isPending状态及时更新,不影响用户交互,比如显示loading加载提示,此时还没有进入过渡更新。 -
ReactSharedInternals
保存共享的初始状态,有React内部维护,此处是获取过渡更新状态。 -
调用
dispatchOptimisticSetState
来更新isPending将值设置为true并调用scheduleUpdateOnFiber
触发fiber更新 -
进入
try catch
逻辑,这里面就是处理过渡更新,并降低优先级的逻辑。会先执行startTransition
包裹的状态更新,并得到返回值returnValue
。 -
判断返回的值是否是Promise对象
- 如果是,则通过
chainThenableValue
来等待Promise执行完成并设置其完成状态。然后调用dispatchSetState
来降低优先级并等待执行 - 如果不是则直接调用
dispatchSetState
来降低优先级并等待执行,在该函数中会调用requestUpdateLane
来获取当前优先级,如果是过渡任务,则会返回过渡优先级。
- 如果是,则通过
-
最后执行
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提示等)
- 降低更新优先级为过渡优先级,延迟状态的更新
题外话 - 号外
本文也是根据这些文章学习进行梳理在自己理解的基础上书写的,如有问题,还请指正。
有兴趣的朋友可以关注一下公众号,方便随时随地一起交流学习。