漫谈 React 系列(五):一起聊一聊 useDeferredValue

7,690 阅读5分钟

前言

在上周 3.29 号,React18 正式版本发布,相信不少小伙伴已经开始体验 18 版本提供的新特性了吧。新版本中最引人瞩目的就属 Concurrent 模式了。在 Concurrent 模式下更新对应的 reconcile 过程是可中断的,这就使得浏览器渲染进程不会被一个耗时较长的 reconcile 阻塞而导致页面在交互过程中出现卡顿,并且可让更高优先级的更新优先处理。

在上一篇 漫谈 React 系列(四): React18 自己的防抖节流 - useTransition 中,我们已经了解到触发 Concurrent 模式需要使用 useTransition。除了 useTransitionReact18 还提供了另外一个 api - useDeferredValue,同样可以触发 Concurrent 模式。

今天,我们就和大家一起聊一聊 useDeferredValue

本文的目录结构如下:

初次体验 useDeferredValue

关于 useDeferredValue,React18 的 changelog 是这样介绍的:

useDeferredValue lets you defer re-rendering a non-urgent part of the tree. It is similar to debouncing, but has a few advantages compared to it. There is no fixed time delay, so React will attempt the deferred render right after the first render is reflected on the screen. The deferred render is interruptible and doesn't block user input.

简单解释一下,就是 useDeferredValue 可以让我们延迟渲染不紧急的部分,类似于防抖但没有固定的延迟时间,延迟的渲染会在紧急的部分先出现在浏览器屏幕以后才开始,并且可中断不会阻塞用户输入。

干巴巴的文字说明,理解起来可能有些晦涩,接下来我们就通过几个简单的 demo 来给大家更形象的展示一下 useDeferredValue 的使用及效果。

示例一

首先,我们通过一个简单的 demo - useDeferredValue - demo - 1 来模拟一个带 search 功能的 Select 组件, 如下:

Apr-05-2022 18-19-44.gif

在上面的示例中,我们使用一个 input 输入框和一个长列表来模拟带 search 功能的 Select 组件,input 输入框受控,长列表有 50000 个节点。在示例中,我们可以看到页面在整个交互过程中有明显的卡顿,这是因为 50000 个节点的 reconcile 过程占据了 js 引擎较多的时间,导致渲染引擎被阻塞。

通常情况下,用户会希望能立即看到自己输入的内容,查询的结果可以稍后展示。基于此,我们可以使用 useDeferredValue 来做优化,将 input 更新作为紧急的部分优先处理,长列表更新作为不紧急的部分延迟处理。

示例二

useDeferredValue - demo - 2 中,我们使用 useDeferredValue 来延迟长列表更新,使用过程和效果如下:

Apr-06-2022 19-19-15.gif

观察上面的示例,我们发现使用 useDeferredValue 之后,input 输入框的交互并没有提升,输入和删除的时候依旧有明显的卡顿。

这是为什么呢?为什么 useDeferredValue 没有生效呢?

我们来分析一波原因,😂。

在上面示例中,虽然我们使用了 useDeferredValue,但并没有发挥出它的效果。我们仅仅延迟了长列表需要的 deferredValue,长列表对应的协调过程并没有延迟。当 input 更新开始协调时,依旧要处理 50000 个节点,使得 js 引擎占据了较多的时间,从而阻塞了渲染引擎。

为了能让 useDeferredValue 能发挥它应有的作用,我们可以将长列表抽离为一个 memo 组件,将 deferredValue 通过 props 传递给长列表组件。当 deferredValue 发生变化时,长列表组件开始更新。

示例三

useDeferredValue - demo - 3 中,我们将长列表抽离为一个 memo 组件,效果如下:

Apr-06-2022 19-34-28.gif

观察示例,我们可以发现将长列表抽离为 memo 组件以后,整个 input 的交互流畅了很多。在整个页面交互过程中,input 更新是高优先级更新,优先处理;而通过 useDeferredValue,长列表更新变为了低优先级更新,延迟处理且可中断。

useDeferredValue 的实现

了解了 useDeferredValue 的用法之后,我们接下来就来看看 useDeferredValue 是怎么实现的。

在上一节关于 useDeferredValue 的介绍中,我们可以提取到几个关键点: 不紧急/紧急延迟可中断。不紧急/紧急,意味着更新优先级低/高;可中断,意味着 Concurrent 模式;延迟,则意味着需要异步触发更新。

基于此,我们可以考虑使用 useTransitionuseEffect 来实现类似 useDeferredValue 的效果,如 useDeferredValue - demo - 3 所示:

Apr-06-2022 19-55-49.gif

整个过程可以归纳为:

  1. 初始化 valuedeferredValue 为空;
  2. 输入 1,通过调用 setValue 触发 input 更新;
  3. input 更新开始处理,value 变为 1,此时 deferredValue 依旧为空;
  4. useEffect 的依赖发生变化,callback 需要处理;长列表 props 没有发生变化,不需要处理;
  5. input 更新渲染完毕,useEffectcallback 触发,开始触发 deferredValue 的更新;
  6. 长列表更新开始处理;

实际上,React18 的源码中 useDeferredValue 的实现过程和我们上面的示例也是一样的,如下:

  // 组件挂载阶段执行 useDeferredValue
  function mountDeferredValue(value) {
    // 定义一个 deferredValue
    var _mountState = mountState(value),
        prevValue = _mountState[0],
        setValue = _mountState[1];
    // 组件挂载阶段执行 useEffect
    mountEffect(function () {
      // 将 setValue 的上下文变为 transition,也就是使用了 useTransition
      var prevTransition = ReactCurrentBatchConfig$2.transition;
      ReactCurrentBatchConfig$2.transition = {};

      try {
        setValue(value);
      } finally {
        ReactCurrentBatchConfig$2.transition = prevTransition;
      }
    }, [value]);
    // 返回旧的 value
    return prevValue;
  }

  // 组件更新阶段执行 useDeferredValue
  function updateDeferredValue(value) {
    var _updateState = updateState(),
        prevValue = _updateState[0],
        setValue = _updateState[1];
    // 组件更新阶段执行 useEffect
    updateEffect(function () {
      var prevTransition = ReactCurrentBatchConfig$2.transition;
      ReactCurrentBatchConfig$2.transition = {};

      try {
        setValue(value);
      } finally {
        ReactCurrentBatchConfig$2.transition = prevTransition;
      }
    }, [value]);
    // 返回旧的 value
    return prevValue;
  }

结束语

到这里,关于 useDeferredValue 的介绍就结束了,相信大家对 useDeferredValue 已经有了初步的认识了吧。如果大家觉得本文还不错,可以给我点个赞哦,😄。