【React Hooks原理 - useDeferredValue】

概述

想必我们在项目中可能使用过Suspense组件,这个组件当其子组件数据未准备好之前会显示占位符内容(后备方案),更加注重用户体验,一般场景就是当我们使用懒加载、异步请求数据会通过该组件添加loading效果。而useDeferredValue则是更加关注渲染性能,在数据未加载之前会返回旧值,并结合memo来减少不必要的渲染,如果不使用memo则优化无效。本文继续通过基本使用和源码来深入学习useDeferredValue的原理

基本使用

useDeferredValue是一个 React Hook,可以让你延迟更新 UI 的某些部分。接收两个参数,并返回旧值:

  • value: 想延迟的值,可以是任何类型。
  • initialValue:仅 Canary 可选 initialValue :在组件初始渲染期间使用的值。如果省略此选项, useDeferredValue 则不会在初始渲染期间延迟,因为它没有可以渲染的 value 先前版本。
  • currentValue (返回值):在初始渲染期间,返回的延迟值将与您提供的值相同。在更新过程中,React 将首先尝试使用旧值重新渲染(因此它将返回旧值),然后在后台尝试使用新值进行另一次重新渲染(因此它将返回更新的值)。

React版本说明:

  • Canary:频繁发布,实验性功能,快速迭代和反馈,不推荐用于生产环境。
  • Alpha:早期测试版本,新功能和改进,功能不够稳定。
  • Beta:功能基本完成,有一些 bug,用于广泛测试和反馈。
  • RC:功能完成,经过广泛测试,主要用于发现和修复小问题,接近最终版本。
  • Stable:最终发布的稳定版本,适用于生产环境,功能稳定可靠

进度链可以这样理解: Canary - Alpha - Beta - RC - Stable(生产环境)

我们都是基于生产环境介绍,所以会省略initialValue参数的说明。

官网Demo:

export default function App() {
  const [query, setQuery] = useState('');
  const deferredQuery = useDeferredValue(query);
  return (
    <>
      <label>
        Search albums:
        <input value={query} onChange={e => setQuery(e.target.value)} />
      </label>
      <Suspense fallback={<h2>Loading...</h2>}>
        <SearchResults query={deferredQuery} />
      </Suspense>
    </>
  );
}

在代码中我们使用到了SuspenseSearchResults,在上面的示例中,输入 “a”,等待结果加载完成,然后将输入框编辑为 “ab”。输入框input值会立即更新,但是SearchResults组件的内容会延迟更新。注意,现在你看到的不是 suspense 后备方案,而是旧的结果列表,直到新的结果加载完成。这是因为suspense是在数据未加载之前显示后备方案,而useDeferredValue在数据未加载之前会返回旧值并且降低更新优先级,此时在suspense看来值已经加载完了只不过是旧值,所以不会显示后备方案。

两者区别如下:

  • Suspense 注重用户体验,处理异步数据或组件加载时的加载状态,提供优雅的占位符
  • useDeferredValue 注重渲染性能,通过延迟低优先级更新保持界面的高响应性,避免界面卡顿。

需要注意的是:

  • SuspenseuseDeferredValue同时使用时,useDeferredValue会导致Suspense后备方案失效
  • useDeferredValue传递给组件时,必须要memo包裹,否则优化失效

源码解析

Mount首次渲染

在首次渲染时,useDeferredValue会接受一个初始值,并返回该初始值作为延迟值(因为首次渲染,没有旧值,而initialValue仅在Canary版本有效)

function mountDeferredValue<T>(value: T, initialValue?: T): T {
  // 初始化创建fiber.hook
  const hook = mountWorkInProgressHook();
  return mountDeferredValueImpl(hook, value, initialValue);
}

//
function mountDeferredValueImpl<T>(hook: Hook, value: T, initialValue?: T): T {
  // 省略了设置initialValue时的情况
  hook.memoizedState = value;
  return value;
}

看代码也很简单,就是在Mount时将value缓存之后就直接返回了,也和上面所说的当初次渲染不设置initialValue时,不会进行延迟更新所符合。

Update更新时

上面说到,当我们更新状态时,该Hook会在数据未返回之前仍然返回旧值 hook.memoizedState,下面来看源码是如何处理的

function updateDeferredValue<T>(value: T, initialValue?: T): T {
  const hook = updateWorkInProgressHook();
  const resolvedCurrentHook: Hook = (currentHook: any);
  const prevValue: T = resolvedCurrentHook.memoizedState;
  return updateDeferredValueImpl(hook, prevValue, value, initialValue);
}
  • updateWorkInProgressHook会复用现有的Hook链表将本次更新任务添加到hook.updateQueue中
  • 获取缓存值,用于对比是否更新
  • 调用updateDeferredValueImpl函数并返回旧值
function updateDeferredValueImpl<T>(
  hook: Hook,
  prevValue: T,
  value: T,
  initialValue?: T
): T {
  if (is(value, prevValue)) {
    return value;
  } else {
    if (isCurrentTreeHidden()) {
      const resultValue = mountDeferredValueImpl(hook, value, initialValue);
      if (!is(resultValue, prevValue)) {
        markWorkInProgressReceivedUpdate();
      }
      return resultValue;
    }
    const shouldDeferValue = !includesOnlyNonUrgentLanes(renderLanes);
    if (shouldDeferValue) {
      const deferredLane = requestDeferredLane();
      currentlyRenderingFiber.lanes = mergeLanes(
        currentlyRenderingFiber.lanes,
        deferredLane
      );
      markSkippedUpdateLanes(deferredLane);
      return prevValue;
    } else {
      markWorkInProgressReceivedUpdate();
      hook.memoizedState = value;
      return value;
    }
  }
}

updateDeferredValueImpl函数主要是降低更新优先级以达到推迟更新的作用,因为调度器会按照优先级高低调度任务。主要分为几部分,我们拆开代码来看:

if (isCurrentTreeHidden()) {
  // Revealing a prerendered tree is considered the same as mounting new
  // one, so we reuse the "mount" path in this case.
  const resultValue = mountDeferredValueImpl(hook, value, initialValue);
  // Unlike during an actual mount, we need to mark this as an update if
  // the value changed.
  if (!is(resultValue, prevValue)) {
    markWorkInProgressReceivedUpdate();
  }
  return resultValue;
}

通过isCurrentTreeHidden来判断当前是否在隐藏树中,一般是Suspense组件包裹。如果在组件中没有使用Suspense组件,则会跳过这段代码。

隐藏树(Hidden Tree)是 React 的一种机制,用于处理组件的懒加载和异步数据加载等场景,确保在数据准备好之前不会渲染不完整的内容。当组件的数据依赖于异步操作(如网络请求)时,React 可能会将当前树标记为“隐藏”,以确保异步操作完成后再进行更新。

在这段代码中表示,如果当前更新发生子隐藏树中时,由于React具有一致性,所以会复用挂载,即执行mountDeferredValueImpl函数重新挂载,将newValue缓存在fiber.memoizedState中,然后判断新旧值是否变化,如果变化会调用markWorkInProgressReceivedUpdate添加更新标记,表示当前fiber需要更新。

这里先通过is(value, prevValue)判断值是否变化,以此来减少不必要的更新,然后再通过!is(resultValue, prevValue)判断是否给当前fiber标记更新是因为mountDeferredValueImpl返回的resultValue可能是initialValue并不是value,上面代码省略了对于initialValue的处理,所以可能会有这个疑问,在这里解释下。

这段代码也能看出会优先执行useDeferredValue, 间接解释了当Suspense组件和useDeferredValue一同使用时,会覆盖其后备方案fallback的执行。

当不存在隐藏树时(即isCurrentTreeHidden() === false),会通过includesOnlyNonUrgentLanes判断当前更新任务是否是紧急优先级:

export function includesOnlyNonUrgentLanes(lanes: Lanes): boolean {
  // TODO: Should hydration lanes be included here? This function is only
  // used in `updateDeferredValueImpl`.
  const UrgentLanes = SyncLane | InputContinuousLane | DefaultLane;
  return (lanes & UrgentLanes) === NoLanes;
}

从代码知道React定义同步、用户输入、默认(普通的更新)都是紧急更新,然后通过位运算,判断当前更新优先级是否是紧急任务。如果不是紧急任务,则shouldDeferValue === false,不需要推迟更新,直接更新缓存值并返回新值,并调用markWorkInProgressReceivedUpdate添加更新标记,等待调度更新。

这里对于优先级低的任务不需要推迟是因为在调度器中会根据优先级高低调度任务,本身低优先级的任务就会延迟执行,所以useDeferredValue主要针对的还是对于高优先级的渲染优化,避免卡顿阻塞交互。

如果高优先级则需要进行推迟更新,即shouldDeferValue === true

// 获取推迟的优先级
const deferredLane = requestDeferredLane();
// 将本次更新优先级和低优先级的推迟优先级合并,以此来降低本次任务的优先级
currentlyRenderingFiber.lanes = mergeLanes(
  currentlyRenderingFiber.lanes,
  deferredLane
);
// 跳过本次更新
markSkippedUpdateLanes(deferredLane);
// 返回旧值
return prevValue;

从上面代码能看出来useDeferredValue推迟更新的本质就是将高优先级的任务和低优先级的推迟优先级合并,以此来降低本次更新的优先级,在调度器中由延迟调用来达到延迟更新的目的。

Demo运行流程

上面介绍了useDeferredValue的基本原理,接下来从一个demo流程来进一步加深对其的理解:

初始状态
  • query 初始值:''
  • deferredQuery 初始值:''
  • isCurrentTreeHidden(): false
第一次输入流程
  1. 用户在输入框中输入一个新值(例如:'a')。
  2. setQuery('a') 被调用,触发 App 组件的重新渲染。
组件重新渲染
  1. query 值变为 'a'
  2. 调用 useDeferredValue(query)
useDeferredValue 执行
  1. 实际调用updateDeferredValue,添加更新任务hook.updateQueue
  2. 调用 updateDeferredValueImpl
updateDeferredValueImpl 执行
  1. is(value, prevValue)
    • prevValue''value'a',两者不同。
  2. isCurrentTreeHidden()false,进入非隐藏树逻辑。
  3. 调用 includesOnlyNonUrgentLanes(renderLanes) 检查优先级。
    • 此时是用户输入为紧急优先级,需要进行延迟更新
  4. requestDeferredLane 获取推迟低优先级。
  5. mergeLanes合并优先级
  6. markSkippedUpdateLanes() 跳过本次更新。
  7. 返回旧值''
Suspense 处理
  1. SearchResults 组件接收到新的 deferredQuery''
  2. Suspense继续显示当前内容,不会显示 fallback,因为 deferredQuery 还是旧值。
第二次输入流程
  1. 用户在输入框中输入另一个新值(例如:'ab')。
  2. setQuery('ab') 被调用,触发 App 组件的重新渲染。
组件重新渲染
  1. query 值变为 'ab'
  2. 调用 useDeferredValue(query)
useDeferredValue 执行
  1. 实际调用updateDeferredValue,添加更新任务hook.updateQueue
  2. 调用 updateDeferredValueImpl
updateDeferredValueImpl 执行
  1. is(value, prevValue)
    • prevValue 为 第一次返回的''value'ab',两者不同。
  2. isCurrentTreeHidden()false,进入非隐藏树逻辑。
  3. 调用 includesOnlyNonUrgentLanes(renderLanes) 检查优先级。
    • 此时是用户输入为紧急优先级,需要进行延迟更新
  4. requestDeferredLane 获取推迟低优先级。
  5. mergeLanes合并优先级
  6. markSkippedUpdateLanes() 跳过本次更新。
  7. 返回旧值''
Suspense 处理
  1. SearchResults 组件接收到新的 deferredQuery''
  2. Suspense继续显示当前内容,不会显示 fallback,因为 deferredQuery 还是旧值。

这也解释了运行上面Demo时,当一直持续输入时,并不会显示Suspense的后备方案,而是显示的'',当我们停止输入请求回来之后才会显示最新的结果。

总结

通过以上流程项目我们对useDeferredValue这个Hook已经非常了解了,这里就简单总结下:这个Hook主要针对高优先级任务进行延迟更新,而之所以能延迟更新是因为通过合并推迟优先级来降低了该更新任务优先级,依赖于调度器根据优先级调度任务的原理

题外话 - 号外

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

有兴趣的朋友可以关注一下公众号,方便随时随地一起交流学习。
在这里插入图片描述

  • 11
    点赞
  • 15
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值