Appearance
useDeferredValue
延迟某个值的更新,使高优先级的任务可以先行完成。
对于 useTransition 可以在密集计算的场景下让 UI 无阻塞, 那么对于 useDeferredValue 来说,其作用跟 useTransition 很相似,也是让高优先级的任务先完成后才去执行当前依赖变量的更新操作。
栗子
- 输入联想搜索场景
jsx
function SearchInput() {
const [value, setValue] = useState("");
const deferredValue = useDeferredValue(value);
const handleSearch = useCallback(
(e) => {
setValue(e.target.value);
},
[value]
);
return (
<div>
<input value={value} onChange={handleSearch} />
<p>value: {value}</p>
<p>deferredValue: {deferredValue}</p>
</div>
);
}这个栗子发现其实其没有看出具体的区别,但是如果我们修改一下 渲染结果, 添加一个长列表渲染,那么我们就可以看出 useDeferredValue 的作用了。
js
function SearchInput() {
const [value, setValue] = React.useState("");
const deferredValue = React.useDeferredValue(value);
const handleSearch = React.useCallback(
(e) => {
setValue(e.target.value);
},
[value]
);
return (
<div>
<input value={value} onChange={handleSearch} />
<p>value: {value}</p>
<p>deferredValue: {deferredValue}</p>
<div>
{Array(10000)
.fill(value)
.map(() => {
return <p>value: {value}</p>;
})}
</div>
</div>
);
}这时候在快速输入的时候就可以看出 value 和 deferredValue 的不同了, deferredValue 的值有些时候会延迟更新。
那么 useDeferredValue 可以优化我们常用的 输入远程搜索 等场景么?
其实一般情况下 这个优化是没有必要的,因为搜索联想等场景,其返回的值的渲染不会占用很长的渲染时间,那么这时候高优先级任务其实很快就执行完成了,仍然会执行新值的渲染操作。
源码详解
我们仍然分为 3 个阶段去各自分析 useDeferredValue 的作用
1. 初次渲染阶段
js
/**
* useDeferredValue 方法 在初次渲染时的实现
* 1. 先将 value 赋值给 hook 对象的 memoizedState 属性
* 2. 返回 value
*/
function mountDeferredValue<T>(value: T): T {
const hook = mountWorkInProgressHook();
hook.memoizedState = value;
return value;
}在初次渲染阶段,useDeferredValue 的实现很简单,就是将 初始化的值 value 赋值给 hook 对象的 memoizedState 属性,然后返回 value。
这样保持在初次渲染阶段可以获取 value 第一次的值, 如上述例子的 defferedValue 就是 ''。
2. 更新渲染阶段
js
function updateDeferredValue<T>(value: T): T {
const hook = updateWorkInProgressHook();
// 获取旧节点
const resolvedCurrentHook: Hook = (currentHook: any);
// 获取旧节点的 value
const prevValue: T = resolvedCurrentHook.memoizedState;
return updateDeferredValueImpl(hook, prevValue, value);
}在更新阶段,useDeferredValue 的实现就比较复杂了,我们先看一下 updateDeferredValueImpl(hook, prevValue, value) 的实现。其三个值分别为 hook : 当前的 hook 对象,prevValue : 旧值,value : 新值。
js
function updateDeferredValueImpl<T>(hook: Hook, prevValue: T, value: T): T {
// 判断是否需要延迟渲染
const shouldDeferValue = !includesOnlyNonUrgentLanes(renderLanes);
if (shouldDeferValue) {
// 包含高优先级的 lane
// 那么就判断新旧值是否发生变化
if (!is(value, prevValue)) {
// Schedule a deferred render
// 如果值发生变化,就需要延迟渲染
// 那么就需要创建一个新的 lane
const deferredLane = claimNextTransitionLane();
// 将新的 lane 添加到当前节点的 lanes 属性中
currentlyRenderingFiber.lanes = mergeLanes(
currentlyRenderingFiber.lanes,
deferredLane
);
// 将新的 lane 添加到根节点 workInProgressRootSkippedLanes 属性中
markSkippedUpdateLanes(deferredLane);
// 将 baseState 设置为 true, 说明需要更新
hook.baseState = true;
}
// Reuse the previous value
// 返回旧的值
return prevValue;
} else {
// 如果进入到 非高优先级渲染阶段
// 判断是否存在更新 如果存在更新 ,那就将 didReceiveUpdate = true
if (hook.baseState) {
// Flip this back to false.
hook.baseState = false;
// 将 didReceiveUpdate = true
markWorkInProgressReceivedUpdate();
}
// 将 hook.memoizedState 设置为最新的 value
hook.memoizedState = value;
// 返回新的值
return value;
}
}其实发现这个实现跟 useState 在更新阶段对于 跳出循环 的逻辑很相似,也是按照以下的流程进行处理的
const shouldDeferValue = !includesOnlyNonUrgentLanes(renderLanes)
判断当然 renderLanes 中是否包含 高优先级的任务( SyncLane | InputContinuousLane | DefaultLane)
- 存在高优先级任务
因为存在高优先级的任务,所以这个更新不会执行,返回的还是之前的值(preValue)。 所以就先判断新旧值是否相等,如果相等就可以不做更新了。
如果不相等的情况下 就去申请一个 transition 类型的车道(claimNextTransitionLane())作为此次延迟更新的 lane, 并将 lane 合并到 函数组件的 lanes 中,等 compeleteWork 阶段收集到 FiberRook 根节点上。例外对于新值(value)其存放在 hook.baseState上,并返回旧值preValue
重点:
- 其新的值存放在
hook.baseState - update 的 lane 是
transition类型的 lane - update 的 lane 存放在 1. 组件 FiberNode.lanes 上 2. 全局变量
workInProgressRootSkippedLanes上
- 不存在高优先级任务
React 任务调度中知道对于每一个 lane 在一定的时候其会进行渲染处理,那么对于 useDefferedValue 产生的 update 的任务,其调度过程是什么?
因为其 lane 为 transition 类型的,所以其优先级很低,超时时间为:25ms。
当进入 lane 的调度渲染的时候,此时会发现没有高优先级的 lane 了,那么进入 else 流程,这边涉及到组件是否更新的判断
js
// 如果进入到 非高优先级渲染阶段
// 判断是否存在更新 如果存在更新 ,那就将 didReceiveUpdate = true
if (hook.baseState) {
// Flip this back to false.
hook.baseState = false;
// 将 didReceiveUpdate = true
markWorkInProgressReceivedUpdate();
}为什么需要判断 hook.baseState 存在的情况下强制更新呐,而不是直接返回新值???
3. 渲染阶段更新
js
function rerenderDeferredValue<T>(value: T): T {
const hook = updateWorkInProgressHook();
if (currentHook === null) {
// This is a rerender during a mount.
hook.memoizedState = value;
return value;
} else {
// This is a rerender during an update.
const prevValue: T = currentHook.memoizedState;
return updateDeferredValueImpl(hook, prevValue, value);
}
}渲染阶段的更新相对比较简单就是判断是否存在 currentHook 去判断是走 mount 还是走 update
注意事项
- 对于 useDeferredValue 来说,其返回的是旧值,所以其在使用的时候需要注意其返回的是旧值,而不是新值。
总结
自此 React18 中两个新的 hook useTransition 和 useDeferredValue 就分析完了,那么这两个 Hook 有什么区别呐,分别解决我们项目中的哪些问题
作用
useTransition
- useTransition 是将一个状态的更新标记为非阻塞更新,让高优先级的任务先执行;
- 只有当你能调用到需要更新的 state 的 set 函数时,才能用这个钩子函数。如果为了响应一个外部传入的 prop 或者自定义钩子函数值,使用 useDeferredValue 函数
- 被标记为 transition 的状态更新会被其他状态更新打断。
- 如果同时有多个 transitions,react 目前会批处理。这个限制可能会在之后解除。
- transition 函数包含的回调应当是同步的,如果有异步需求,将 transition 放在异步函数内;
useDeferredValue
- 将一个状态标记为延迟状态。 其作用跟 节流防抖很像,将一个状态的新值在一定时间后才更新为最新值;
- 首次渲染时,延迟状态和初始状态相同;在更新时,react 首先尝试用旧值重渲染,然后在后台尝试用新值渲染;
- 传递给 hook 的参数应该是初始类型或者在渲染之外的对象,如果是在渲染时创建的新对象,会导致不必要的后台重渲染
异同点
相同
- 都是用来降低我们组件中对于某些优先级不高的任务的执行优先级,从而保证我们的页面的流畅性。
不同点
- 执行对象不同
- useTransition 是一个用来更新 UI 的同时不会阻塞的 react 钩子函数。
- useDeferredValue 是一个用来延迟某个值的更新,使高优先级的任务可以先行完成。
