useTransition真的无所不能吗?(一)

简介: useTransition真的无所不能吗?(一)

人生不售来回票,一旦动身,绝不能复返

大家好,我是柒八九

前言

之前通过React 并发原理讲解了React如何实现原理。但是在应用层面涉及的不多,而今天我们就对如何正确的使用并发渲染做进一步的梳理。而提起并发渲染,useTransitionuseDeferredValue是我们绕不过去的两座大山。

useTransitionuseDeferredValue为我们提供了对过渡的控制,它被认为对我们的UI交互性能将产生革命性的影响。

既然,人家都说是革命性的改变,那是不是我们可以在任何场景使用?是否有一些桎梏?是否有一些让人匪夷所思的特性和”癖好“。让我们今天就对这些进一步讨论和分析。

还有有一句话,希望大家谨记:

并发渲染钩子会导致重新渲染。因此,永远不要在所有状态更新中使用它们

题外话

话说,你们除夕上班吗?


好了,天不早了,干点正事哇。


我们能所学到的知识点

  1. 前置知识点
  2. 案例分析
  3. 并发渲染和useTransition
  4. useTransition会导致重新渲染
  5. 如何正确的使用useTransition
  6. useDeferredValue
  7. debounce VS useTransition

1. 前置知识点

前置知识点,只是做一个概念的介绍,不会做深度解释。因为,这些概念在下面文章中会有出现,为了让行文更加的顺畅,所以将本该在文内的概念解释放到前面来。如果大家对这些概念熟悉,可以直接忽略


同时,由于阅读我文章的群体有很多,所以有些知识点可能我视之若珍宝,尔视只如草芥,弃之如敝履。以下知识点,请酌情使用

useTransition的使用

首先,确保你的项目已经升级到 React 18 或更高版本。

并且,在你的组件的顶层调用useTransition,以将某些状态更新标记为过渡。

import { useTransition } from 'react';
function Container() {
  const [isPending, startTransition] = useTransition();
  // ...
}

参数

useTransition 不接受任何参数。

返回值

useTransition 返回一个包含两个项的数组:

  1. isPending 标志,用于告诉你是否有待处理的过渡。
  2. startTransition 函数,允许你将状态更新标记为过渡。

2. 案例分析

首先,我们用vite构建一个react-ts项目。

yarn create vite useTransiont --template react-ts

(墙裂推荐大家手动实践一下)

大体的页面结构如下:

image.png

我们将拥有一个App组件,它渲染三个buttonABC),并有条件地渲染这些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启动前端项目,其大致的页面结果如下:


image.png

我们假设B组件是一个耗时组件,它在内部渲染了100个小组件,并且每个组件需要花费大约10毫秒来渲染。理论上来说,渲染100个组件对React来说小菜一碟,但架不住每个组件需要10毫秒。那就得到一个糟糕的结果,渲染B页面将需要1秒钟。


B组件代码

import { ItemsList } from "@components/SlowComponents";
export const B = () => {
  console.log("B被触发了");
  useLogOnRender("B");
  return (
    <div className="projects">
      此组件需要展示大量的耗时内容
      <br /> <br /> <ItemsList />
    </div>
  );
};

B组件渲染的子组件(耗时组件)

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。在快速切换的过程中,从BC过程中页面会有不定时间的卡顿。

本来你想快速的看到C的内容,但是浏览器却对你说:丞妾做不到


但是,作为精益求精的用户,容不得眼里有一点沙子。用户可不会惯着你,虽然今天是1024(本文起稿日期),但是,小可爱的产品经理,要让你把这个东西给优化处理掉。让用户在访问页面时,有一种像吃了德芙般丝滑的体验。

但是,你思来想去,发现你的武器库中缺失了这种利器。你不好去做优化处理。

这是因为,虽然React状态更新并不是异步的(我们之前的文章有讲过,有兴趣的可以翻找一下)。触发状态更新通常是异步的:我们会在各种回调函数中异步触发它,以响应用户交互。但一旦状态更新被触发,React会义无反顾同步地计算所有必要的更新,重新渲染所有需要重新渲染的组件,将这些更改提交到DOM,以便它们显示在屏幕上。

如果在这期间点击了一个Button按钮,该操作导致的状态更新将被放入任务队列中,在主任务(慢状态更新)完成后执行。

我们可以在控制台输出中看到这种行为:通过点击Button触发的所有重新渲染都将被记录,即使在此期间屏幕被冻结。

点击的顺序为A->B->C

image.png


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布尔值来添加一个加载状态,以表示等待更新完成的过程中正在发生某些事情。

我们把之前的代码稍微粉饰一下:

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 ? " 🤔..." : ""}
    </button>
  );
};

当我点击B按钮时,加载指示器会出现,如果我立即点击C,我会立即切换到我们想要展示的页面内容。浏览器没有发生页面卡顿。


相关文章
|
2月前
|
传感器 算法 机器人
斯坦福李飞飞团队祭出“灵巧手”,泡茶剪纸炫技
【2月更文挑战第26天】斯坦福李飞飞团队祭出“灵巧手”,泡茶剪纸炫技
31 5
斯坦福李飞飞团队祭出“灵巧手”,泡茶剪纸炫技
|
5月前
|
JavaScript 前端开发 Java
二十年编程语言风云,哪款是你的爱豆?
二十年编程语言风云,哪款是你的爱豆?
|
5月前
|
前端开发 安全 C++
useTransition真的无所不能吗?(二)
useTransition真的无所不能吗?(二)
|
Web App开发 Windows
推荐5款让你相见恨晚的神级软件,把把直击心灵
今天来给大家推荐5款良心软件,每款都是经过时间检验的精品,用起来让你的工作效率提升飞快,各个都让你觉得相见恨晚!
215 0
推荐5款让你相见恨晚的神级软件,把把直击心灵
|
Web App开发 人工智能 自然语言处理
天网是用COBOL写的?1984年《终结者》竟然用了他的代码!
近日,推特上一则COBOL代码出现在《终结者》中的消息引发了热议,原作者兴奋不已还找到了当年的原文公布在网上,网友纷纷提议找《终结者》要版税。老COBOLER原来是一名技术极客,今天我们就来看看他传奇的技术人生。
207 0
天网是用COBOL写的?1984年《终结者》竟然用了他的代码!
|
Java 程序员 应用服务中间件
深受程序员鄙视的外行语录,你被哪句话打击过?
最近,有几位程序员朋友,向我吐槽,经常有人和他说一些让他恼怒的话,他听完都想打人啦。我听完之后也是哭笑不得,将这些程序员朋友的话给整理出来了,大家以后在程序员面前说话可得小心点。
1146 0
|
新零售
问马云:6个最犀利的问题
这几天,马老师很忙。他从乌镇去了上海,又马不停蹄地去了广州,出席了“广州2017财富全球论坛”。 这个论坛,主持人和观众连番发问,比如“阿里巴巴最大的竞争对手是谁?”、“如何保证阿里巴巴不消失?” 我们选择了六个最犀利的问题。请看看,面对这些问题,马老师是怎么回答的。
3398 0