Skip to content

useDeferredValue

延迟某个值的更新,使高优先级的任务可以先行完成。

对于 useTransition 可以在密集计算的场景下让 UI 无阻塞, 那么对于 useDeferredValue 来说,其作用跟 useTransition 很相似,也是让高优先级的任务先完成后才去执行当前依赖变量的更新操作。

栗子

  1. 输入联想搜索场景
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 在更新阶段对于 跳出循环 的逻辑很相似,也是按照以下的流程进行处理的

  1. const shouldDeferValue = !includesOnlyNonUrgentLanes(renderLanes)

判断当然 renderLanes 中是否包含 高优先级的任务( SyncLane | InputContinuousLane | DefaultLane

  1. 存在高优先级任务

因为存在高优先级的任务,所以这个更新不会执行,返回的还是之前的值(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
  1. 不存在高优先级任务

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

注意事项

  1. 对于 useDeferredValue 来说,其返回的是旧值,所以其在使用的时候需要注意其返回的是旧值,而不是新值。

总结

自此 React18 中两个新的 hook useTransitionuseDeferredValue 就分析完了,那么这两个 Hook 有什么区别呐,分别解决我们项目中的哪些问题

作用

useTransition

  • useTransition 是将一个状态的更新标记为非阻塞更新,让高优先级的任务先执行;
  • 只有当你能调用到需要更新的 state 的 set 函数时,才能用这个钩子函数。如果为了响应一个外部传入的 prop 或者自定义钩子函数值,使用 useDeferredValue 函数
  • 被标记为 transition 的状态更新会被其他状态更新打断。
  • 如果同时有多个 transitions,react 目前会批处理。这个限制可能会在之后解除。
  • transition 函数包含的回调应当是同步的,如果有异步需求,将 transition 放在异步函数内;

useDeferredValue

  • 将一个状态标记为延迟状态。 其作用跟 节流防抖很像,将一个状态的新值在一定时间后才更新为最新值;
  • 首次渲染时,延迟状态和初始状态相同;在更新时,react 首先尝试用旧值重渲染,然后在后台尝试用新值渲染;
  • 传递给 hook 的参数应该是初始类型或者在渲染之外的对象,如果是在渲染时创建的新对象,会导致不必要的后台重渲染

异同点

相同

  1. 都是用来降低我们组件中对于某些优先级不高的任务的执行优先级,从而保证我们的页面的流畅性。

不同点

  1. 执行对象不同
  • useTransition 是一个用来更新 UI 的同时不会阻塞的 react 钩子函数。
  • useDeferredValue 是一个用来延迟某个值的更新,使高优先级的任务可以先行完成。