React 18中的useTransition和useDeferredValue使用

React18 引入了一个关键概念 并发性(Concurrency)。 并发则涉及到多个更新操作的同时执行,这可以说是React18中最重要的功能。
除了并发,React18 新增了两个hook ,也就是useTransitionuseDeferredValue。它们的作用都是降低更新操作的优先级,但问题是,何时应该使用它们?

并发(Concurrent)

在实现"并发"之前,渲染是同步的(所谓的同步,就是指如果react的某个组件执行时间长,它无法中断,会一直执行,直到组件完全渲染到DOM中。在这个过程中,由于Javascript是单线程的,因此渲染任务会占满JavaScript线程,阻塞浏览器的主线程,从而导致用户无法进行交互操作)。

然而,有了并发渲染(并发指的就是通过time slice将任务拆分为多个,然后react根据优先级来完成调度策略,将低优先级的任务先挂起,将高优先级的任务分配到浏览器主线程的一帧的空闲时间中去执行,如果浏览器在当前一帧中还有剩余的空闲时间,那么React就会利用空闲时间来执行剩下的低优先级的任务),react的渲染和更新可以被中断和恢复。那么如果在执行某个组件更新过程中又有了新的更新请求到达。比如我们下面的input输入事件,那么React就会创建一个新的更新版本。这种情况下,在某个时间段内可能会同时存在多个更新版本

为了优化上述问题,React 18 提供了新的 Hook 函数 useTransition,它可以将多个版本的更新打包到一起,在未来的某一帧空闲时间内执行,从而优化应用的性能和响应时间。而useDeferredValue 的作用是将某个值的更新推迟到未来的某个时间片内执行,从而避免不必要的重复渲染和性能开销。

没有使用任何优化手段,同步更新

假设我们有一个包含从0到19,999数字的数组。这些数字在用户界面上显示为一个列表。该用户界面还有一个文本框,允许我们过滤这些数字。例如,我可以通过在文本框中输入数字99来过滤掉以99开头的数字。

import { useState, useTransition } from "react";

const numbers = [...new Array(20000).keys()];

export default function App() {
    const [query, setQuery] = useState("");

    const handleChange = (e) => {
        setQuery(e.target.value);
    };

    return (
        <div>
            <input type="number" onChange={handleChange}/>
            <div>
                {
                    numbers.map((i, index) => (
                        query
                            ? i.toString().startsWith(query)
                            && <p key={index}>{i}</p>
                            : <p key={index}>{i}</p>
                    ))
                }
            </div>
        </div>
    );
}

由于数组中有20,000个元素,过滤将是一个有点耗时的过程。当我们试图在文本框中输入一个数字时,我们可以观察到这一点。输入的数值出现在文本框中会有一个滞后,因为每一个按键之后的渲染都会花费一些时间。

useTransition

接下来我们使用useTransition来修改一下上面的代码

function App() {
    const [query, setQuery] = useState("");
    const [isPending, startTransition] = useTransition();

    const handleChange = (e) => {
        startTransition(() => {
            setQuery(e.target.value);
        });
    };

    const list = useMemo(() => (
        numbers.map((i, index) => (
            query
                ? i.toString().startsWith(query)
                && <p key={index}>{i}</p>
                : <p key={index}>{i}</p>
        ))
    ), [query]);

    return (
        <div>
            <input type="number" onChange={handleChange} />
            <div>
                {
                    isPending
                        ? "Loading..."
                        : list
                }
            </div>
        </div>
    );
}

从上面可以看到useTransation返回一个包含两个子项的数组。

isPending: 告诉你目前是否有一些更新操作任然在等待中(尚未被React执行,并以较低的优先级处理)
startTransition: React会以一个较低的优先级调度被它包装的更新操作

这样,就确保了用户和输入框的交互操作保持流畅。然后再通过isPending来判断是否可以更新UI。

useDeferredValue

useDeferredValue的作用和useTransition一致,都是用于在不阻塞UI的情况下更新状态。但是使用场景不同。

useTransition是让你能够完全控制哪个更新操作应该以一个比较低的优先级被调度。但是,在某些情况下,可能无法访问实际的更新操作(例如,状态是从父组件上传下来的)。这时候,就可以使用useDeferredValue来代替。

用React 团队成员Dan的话说useDeferredValue主要是:

useful when the value comes “from above” and you don’t actually have control over the corresponding setState call.

它的意思就是: 当值来自 "上层",而你实际上不能控制相应的setState调用时,这个方法很有用。

这就比较契合我们上面所举例子的场景。

那我们就需要将上面的例子改成如下:

import { useState, useMemo, useDeferredValue } from "react";

const numbers = [...new Array(200000).keys()];

export default function App() {
    const [query, setQuery] = useState("");

    const handleChange = (e) => {
        setQuery(e.target.value);
    };

    return (
        <div>
            <input type="number" onChange={handleChange} value={query} />
            <List query={query} />
        </div>
    );
}

function List(props) {
    const { query } = props;
    const defQuery = useDeferredValue(query);

    const list = useMemo(() => (
        numbers.map((i, index) => (
            defQuery
                ? i.toString().startsWith(defQuery)
                && <p key={index}>{i}</p>
                : <p key={index}>{i}</p>
        ))
    ), [defQuery]);

    return (
        <div>
            {list}
        </div>
    );
}

总结

如上所述,useTransition直接控制更新状态的代码,而useDeferredValue控制一个受状态变化影响的值。它们做的是同样的事,帮助提高用户体验(UX),不应该同时使用这两者。

相反,如果你可以访问更新操作,并且有一些更新操作应该以较低的优先级处理,就使用useTransition。如果你没有这种权限,就使用useDeferredValue

参考

useTransition and useDeferredValue in React 18
UseTransition() Vs UseDeferredValue() In React 18

301 声望
103 粉丝
0 条评论
推荐阅读
「多图预警」完美实现一个@功能
一天产品大大向 boss 汇报完研发成果和产品业绩产出,若有所思的走出来,劲直向我走过来,嘴角微微上扬。产品大大:boss 对我们的研发成果挺满意的,balabala...(内心 OS:不听,讲重点)产品大大:咱们的客服 I...

wuwhs40阅读 4.8k评论 5

封面图
ESlint + Stylelint + VSCode自动格式化代码(2023)
安装插件 ESLint,然后 File -&gt; Preference-&gt; Settings(如果装了中文插件包应该是 文件 -&gt; 选项 -&gt; 设置),搜索 eslint,点击 Edit in setting.json

谭光志34阅读 20.8k评论 9

涨姿势了,有意思的气泡 Loading 效果
今日,群友提问,如何实现这么一个 Loading 效果:这个确实有点意思,但是这是 CSS 能够完成的?没错,这个效果中的核心气泡效果,其实借助 CSS 中的滤镜,能够比较轻松的实现,就是所需的元素可能多点。参考我们...

chokcoco24阅读 2.3k评论 3

你可能不需要JS!CSS实现一个计时器
CSS现在可不仅仅只是改一个颜色这么简单,还可以做很多交互,比如做一个功能齐全的计时器?样式上并不复杂,主要是几个交互的地方数字时钟的变化开始、暂停操作重置操作如何仅使用 CSS 来实现这样的功能呢?一起...

XboxYan25阅读 1.7k评论 1

封面图
在前端使用 JS 进行分类汇总
最近遇到一些同学在问 JS 中进行数据统计的问题。虽然数据统计一般会在数据库中进行,但是后端遇到需要使用程序来进行统计的情况也非常多。.NET 就为了对内存数据和数据库数据进行统一地数据处理,发明了 LINQ (L...

边城17阅读 2k

封面图
【代码鉴赏】简单优雅的JavaScript代码片段(一):异步控制
Promise.race不满足需求,因为如果有一个Promise率先reject,结果Promise也会立即reject;Promise.all也不满足需求,因为它会等待所有Promise,并且要求所有Promise都成功resolve。

csRyan26阅读 3.4k评论 1

「彻底弄懂」this全面解析
当一个函数被调用时,会创建一个活动记录(有时候也称为执行上下文)。这个记录会包含函数在 哪里被调用(调用栈)、函数的调用方法、传入的参数等信息。this就是记录的其中一个属性,会在 函数执行的过程中用到...

wuwhs17阅读 2.4k

封面图
301 声望
103 粉丝
宣传栏