导读

React18 起新加入了 useTransition 方法,

这个方法能解决什么问题,我们又如何使用它实现更优秀的产品体验?

既然文章标题说到了“防抖节流”,那这个方法和防抖节流有什么关系呢,会是同样技术思路的内部封装吗?

​我们从防抖节流这项前端技术人必备的小技巧切入,来看看新方法 useTransition 的面纱。

再谈防抖节流

在绝大多数的产品交互中都都存在着两种数据反馈。一种是当我点击一个按钮,我希望它给我一份最新的数据;另一种是当我不断点击一个按钮的时候,我希望它始终给我最新的数据。在用户的角度,这两者的体验是具有一致性的,即当我“发生动作”时,我都希望尽快拿到最新的反馈结果。但在技术上,这两者是有巨大不同的,在 web 的世界里,数据响应是异步的,当动作持续发生时,如何让最新结果更快地反馈到用户,既需要考虑数据请求触发的优化,也需要考虑重复渲染成本的优化。

所以,我们引入两个概念来区分。我们把直接的交互反馈叫做「紧急更新」,典型的场景如点击按钮提交数据;把可能持续性触发的交互反馈叫做「过渡更新」,典型的场景如持续拖拽浏览器窗口重算布局、输入框持续输入触发联想请求。

如果技术研发中不关注对「过渡更新」的处理,那么不同严重程度上都会造成

  1. 大量高频请求造成的服务器开销
  2. 重复渲染计算造成的卡顿
  3. 在用户网络稍差的情况的持续“卡顿”

然而在 React17 及之前的版本,防抖节流是没有内置的解决方案的,需要用户手动实现或是利用第三方库来处理,所以我们常常看到很多线上项目理所当然地“偷懒”了。毕竟各位研发的电脑都是高配置,在没有强规范和足够的环境引导下,很难说提升整个团队对外的基础能力,当框架去推动这件事,就意味着所有上层应用都能往交互体验更好地走一步。

React18 中的防抖节流

虽然从开篇到现在我依然在提防抖节流,但 React18 中的新方法 useTransition 并不是防抖节流。

为什么这么说呢,因为「防抖节流」是一个手段,一个解决问题的技术方案,它既不是场景、也不是问题。真正的问题是如何优化过渡更新,以减少重算和提升效率,最终达到一个更好的体验


让我们来回顾一下防抖和节流:

  • 防抖的思路是限定时间等待状态稳定,举个例子就像 等电梯关门,一旦有一个人进来就需要再等1分钟才关门。
  • 节流的思路是固定间隔时间执行,举个例子就像 等红灯通行,每等1分钟的红灯过后再通行。

你可能也发现了,不管防抖还是节流都有一个问题是,他们依赖一个研发设定的「间隔时间」,这意味着我们(研发)需要评估和猜测绝大部分用户在多少间隔时间是合适的,但事实是,不同的电脑性能、不同的网络状态所面临的瓶颈是天差地别的,比如在我们优秀的电脑上查看自己研发的产品就不会卡,对吧

新的实现 useTransition 正解决了这一问题,实现了根据用户本机的实际情况进行“充足” 的计算。其底层是 React18 对其所有工作进行每隔5毫秒的拆分和短暂挂起以让浏览器执行其他工作,并且优化了渲染队列,更高效地对新渲染需求进行处理。

image.png

使用案例

方法解释

一个基础调用是这样的

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
import { useTransition } from 'react';

export default () => {
const [startTransition, isPending] = useTransition({
timeoutMs: 3000
});

return (
<>
{ article }
<button
onClick={() => {
startTransition(() => {
// example
const article = getSomeData();
setArticle(article);
});
}}
>
获取内容
</button>
</>
)
};

startTransition 是一个函数,用它来包装需要过渡/延迟更新的内容

isPending 是一个布尔值,用它来判断这种过渡/延迟更新是否正在进行

在上面这个代码段中,3000ms 表示如果获取数据响应超过了这个时间并且有下一个更新,React 将中断当前的更新。当然在这个案例中,你也同样可以选择用 isPending 对 button 进行 disabled 的处理。

查看完整的官方案例,展示了在反复触发点击获取数据后如何更优的更新渲染结果



升级到 React18 的 createRoot 形式

截至 21/08 是 alpha 版本,预计如果你看到这篇内容是在 21/11 之后,请自行查下最新包的升级方式

1
yarn add react@alpha react-dom@alpha

找到根节点注入的方式并修改

将原本的 render 方法更新为 createRoot,React18 中的大部分新特性都需要改到这个并发模式

1
2
3
4
5
6
// index.js before
ReactDOM.render(<App />, document.getElementById('root'));

// index.js after
const root = ReactDOM.createRoot(document.getElementById('root'));
root.render(<App />);


实现一个实时输入搜索

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
import React, { useState, useTransition } from "react";
import ReactDOM from "react-dom";

import "./styles.css";
import { fetchSearch } from "./fakeApi";

function App() {
const [search, setSearch] = useState([]);
const [startSearch, isSearching] = useTransition({
timeoutMs: 2000
});

return (
<>
<div>
<input
onChange={(e) => {
const string = e.target.value.trim();
startSearch(async () => {
if (!string) {
setSearch([]);
} else {
const rst = await fetchSearch(string);
setSearch(rst);
}
});
}}
/>
</div>
<div>
{search.map((item, i) => (
<div key={i}>{item}</div>
))}
</div>
</>
);
}

const rootElement = document.getElementById("root");
ReactDOM.createRoot(rootElement).render(<App />);

CodeSandBox

延伸:更新阻断带来的隐患

由于 Transition 的实现中基于多更新任务的对比计算和中断,在大体量的更新内容下可能有一定的风险性,需要进一步测试。目前测试的代码场景是简明扼要的,真实场景实际关联的组件量很可能众多而复杂,这种依赖于短时间对比计算的设计可能有一定的风险,这种场景在技术方案上或许还是更适合新的 Suspense 方案

延伸:开发小技巧1 —— 人为制造卡顿

通过 DevTool 可以调整模拟更差的网络情况和 CPU 情况,开始录制后的操作可能得到一份十分痛苦的产品体验

image.png