❝人生不售来回票,一旦动身,绝不能复返 ❞
大家好,我是「柒八九」。
前言
之前通过React 并发原理讲解了React
如何实现原理。但是在应用层面涉及的不多,而今天我们就对如何正确的使用并发渲染做进一步的梳理。而提起并发渲染,useTransition
和useDeferredValue
是我们绕不过去的两座大山。
❝
useTransition
和useDeferredValue
为我们提供了对「过渡的控制」,它被认为对我们的UI交互性能将产生革命性的影响。 ❞
既然,人家都说是革命性的改变,那是不是我们可以在任何场景使用?是否有一些桎梏?是否有一些让人匪夷所思的特性和”癖好“。让我们今天就对这些进一步讨论和分析。
还有有一句话,希望大家谨记:
❝并发渲染钩子会导致「重新渲染」。因此,永远不要在所有状态更新中使用它们 ❞
题外话
话说,你们除夕上班吗?
好了,天不早了,干点正事哇。
1. 前置知识点
❝「前置知识点」,只是做一个概念的介绍,不会做深度解释。因为,这些概念在下面文章中会有出现,为了让行文更加的顺畅,所以将本该在文内的概念解释放到前面来。「如果大家对这些概念熟悉,可以直接忽略」 同时,由于阅读我文章的群体有很多,所以有些知识点可能「我视之若珍宝,尔视只如草芥,弃之如敝履」。以下知识点,请「酌情使用」。 ❞
useTransition的使用
首先,确保你的项目已经升级到 React 18
或更高版本。
并且,在你的组件的顶层调用useTransition
,以将某些状态更新标记为过渡。
import { useTransition } from 'react';
function Container() {
const [isPending, startTransition] = useTransition();
// ...
}
参数
useTransition
不接受任何参数。
返回值
useTransition
返回一个包含两个项的数组:
isPending
标志,用于告诉你是否有待处理的过渡。startTransition
函数,允许你将状态更新标记为过渡。
2. 案例分析
首先,我们用vite
构建一个react-ts
项目。
yarn create vite useTransiont --template react-ts
(墙裂推荐大家手动实践一下)
大体的页面结构如下:
我们将拥有一个App
组件,它渲染三个button
(A
、B
和C
),并有条件地渲染这些Button
所对应的内容。App
将保持切换Button
的状态并渲染正确的组件。
export default function App() {
const [tab, setTab] = useState("A");
return (
<div className="container">
<div className="btns">
<Button isActive={tab === "A"} onClick={() => setTab("A")} name="A" />
<Button isActive={tab === "B"} onClick={() => setTab("B")} name="B" />
<Button isActive={tab === "C"} onClick={() => setTab("C")} name="C" />
</div>
<div className="content">
{tab === "A" && <A />}
{tab === "B" && <B />}
{tab === "C" && <C />}
</div>
</div>
);
}
使用yarn dev
启动前端项目,其大致的页面结果如下:
我们假设B
组件是一个「耗时组件」,它在内部渲染了100
个小组件,并且每个组件需要花费大约10
毫秒来渲染。理论上来说,渲染100
个组件对React
来说小菜一碟,但架不住每个组件需要10
毫秒。那就得到一个糟糕的结果,渲染B
页面将需要1秒钟。
B组件代码
代码语言:javascript复制import { ItemsList } from "@components/SlowComponents";
export const B = () => {
console.log("B被触发了");
useLogOnRender("B");
return (
<div className="projects">
此组件需要展示大量的耗时内容
<br /> <br /> <ItemsList />
</div>
);
};
B组件渲染的子组件(耗时组件)
代码语言:javascript复制const SlowItem = ({ id }: { id: number }) => {
const startTime = performance.now();
while (performance.now() - startTime < 10) {
// 模拟耗时任务,让主线程暂停10ms
}
return <li className="item">耗时任务 #{id 1}</li>;
};
export const ItemsList = () => {
const items = [...Array(100).keys()];
return (
<ul className="items">
{items.map((id) => (
<SlowItem id={id} />
))}
</ul>
);
};
现在尝试在这些Button
之间快速切换。如果我尝试从A
切换到B
,然后立刻切换到C
。在快速切换的过程中,从B
到C
过程中页面会有不定时间的卡顿。
本来你想快速的看到C
的内容,但是浏览器却对你说:「丞妾做不到」
但是,作为「精益求精」的用户,容不得眼里有一点沙子。用户可不会惯着你,虽然今天是1024
(本文起稿日期),但是,小可爱
的产品经理,要让你把这个东西给优化处理掉。让用户在访问页面时,有一种像吃了德芙般丝滑的体验。
但是,你思来想去,发现你的「武器库」中缺失了这种利器。你不好去做优化处理。
这是因为,虽然React
状态更新并不是异步的(我们之前的文章有讲过,有兴趣的可以翻找一下)。「触发状态更新通常是异步」的:我们会在各种回调函数中异步触发它,以响应用户交互。但一旦状态更新被触发,React
会义无反顾「同步地计算所有必要的更新,重新渲染所有需要重新渲染的组件」,将这些更改提交到DOM
,以便它们显示在屏幕上。
如果在这期间点击了一个Button
按钮,该操作导致的「状态更新将被放入任务队列中」,在主任务(慢状态更新)完成后执行。
我们可以在控制台输出中看到这种行为:通过点击Button
触发的「所有重新渲染都将被记录」,即使在此期间屏幕被冻结。
点击的顺序为A
->B
->C
3. 并发渲染和useTransition
❝关于并发的内容,这篇文章中不打算过多的涉及,有兴趣的可以参考之前的文章React 并发原理 ❞
上文讲到通过常规的React
更新方式,不能很好的处理上面页面卡顿的现象。而React
官方也注意到这种情况。所以,它们为我们带来了,新的渲染方式和API
来处理上面的顽疾。
我们先下一个结论。
❝
并发渲染
和useTransition
用于处理缓慢的状态更新 ❞
通过并发渲染
,我们可以「明确标记某些状态更新和由它们引起的重新渲染为“非关键”」。因此,React
会在「后台」计算这些更新,而「不会阻塞主任务」。如果发生关键
事件(即正常状态更新),React
将暂停其后台
渲染,执行关键更新,然后「要么返回到先前的任务,要么完全放弃它并启动一个新任务」。
❝“后台”是一种数据的抽象:有几点需要说明
- 由于
JavaScript
是单线程的。在繁忙的“后台”任务执行过程中,React
将定期检查主队列。如果队列中出现新的任务,它将优先于“后台”工作。(这种消息通知是利用MessageChannel
,关于这点可以参考我们之前的文章React 并发原理) - 在后台渲染的是一种叫做
Fiber
的数据结构(关于这点可以参考我们之前的文章React_Fiber机制(上)/React_Fiber机制(下))
❞
回到上面的问题,在之前的代码中,我们遇到的情况是,点击button
渲染对应的内容时,其中一个组件(B
)非常慢并且阻塞用户交互,而这种情况正好撞到了并发渲染
的枪口上了,它的出现就是为了解决这种情况的。而我们现在要做的就是将B
组件的渲染标记为「非关键」。
我们可以使用useTransition
钩子来实现这一点。
- 它返回一个
loading
布尔值作为第一个参数 - 以及一个函数作为第二个参数。
- 在这个函数内部,我们将调用
setTab("B")
- 从此时开始,该状态更新将在“后台”计算,而不会阻塞页面。
- 在这个函数内部,我们将调用
此外,我们可以使用isPending
布尔值来添加一个加载状态,以表示等待更新完成的过程中正在发生某些事情。
我们把之前的代码稍微粉饰一下:
代码语言:javascript复制export default function App() {
const [tab, setTab] = useState('B');
// 添加useTransition钩子
const [isPending, startTransition] = useTransition();
return (
<div className="container">
<div className="btns">
...
<Button
// 表示内容正在加载
isLoading={isPending}
onClick={() => {
// 在传递给startTransition的函数中调用setTab
startTransition(() => {
setTab('B');
});
}}
name="B"
/>
...
</div>
...
</div>
);
}
这样就实现了「通过并发渲染将耗时渲染的内容标记为非关键」,从而改善用户体验。
同时,我们需要改造一下Button
组件,让其能够接收表示过渡状态的isPending
type ButtonProps = {
isActive?: boolean;
isLoading?: boolean;
name: string;
onClick: () => void;
};
export const Button = ({ name, onClick, isActive, isLoading }: ButtonProps) => {
return (
<button
onClick={onClick}
className={`tab-button ${isActive ? "active" : ""}`}
>
{name}
{isLoading ? "