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

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

4. useTransition会导致重新渲染

通过,对第一段代码施以useTransition的魔法,让其从半身不遂变的行动自如。此时,你感觉到一切都是向着美好的方向前行着,但是事实哪有那么的顺心遂意。

在现实生活中,这些Button中的任何一个都可能非常耗时。此时,你也无法预知到底哪个Button是耗时的。此时你的双脚离地了,病毒就关闭了,聪明的智商又占领高地了


所以,你就将所有这些Button之间的过渡都标记为非关键,并在其中的startTransition中更新状态。

并且,为了体现自己的代码功底,他还贴心的把过渡过程封装成了一个函数

const onBtnClick = (btn) => {
  startTransition(() => {
    setTab(btn);
  });
};

然后在所有按钮上使用这个函数,而不是直接设置状态:

<Button
  isActive={tab === "A"}
  onClick={() => onBtnClick("A")}
  name="A"
/>
<Button
  isActive={tab === "B"}
  onClick={() => onBtnClick("B")}
  name="B"
/>
<Button
  isActive={tab === "C"}
  onClick={() => onBtnClick("C")}
  name="C"
/>

所有来自这些按钮的状态更新现在都将被标记为非关键

在运行代码后,我们发现又出现了新的问题:

如果我们从B页面切换到AC,不再是瞬间发生了!而我们对天发誓没有改变这两个页面上的任何东西,它们目前都只渲染一个字符串,但它们都表现得好像非常耗时。


这里的问题在于,

如果我们将状态更新包装在一个过渡中,React并不只是在"后台"触发状态更新。实际上,这是一个两步过程

  1. 首先,会触发一个立即关键重新渲染,使用从useTransition钩子中提取的isPending布尔值从false变为true。(我们能够在渲染输出中使用它的事实应该是一个重要的线索。)
  2. 只有在这个关键的重新渲染完成后,React才会开始进行非关键状态更新。

简而言之,useTransition导致两次重新渲染,而不是一次。因此,我们看到了上面示例中的行为。如果我在B页面上,并点击A Button,首先触发的是初始重新渲染,此时B Button还是选中状态。非常耗时的B组件在重新渲染时阻塞了主任务1秒钟。只有在这之后,才会安排并执行从BA的非关键状态更新。

点击顺序 B->A


5. 如何正确的使用useTransition

记忆所有的值

为了解决上述性能下降的问题,我们需要确保额外的第一次重新渲染尽可能轻量。通常,这意味着我们需要对可能导致它减速的一切进行记忆化处理:

  • 所有耗时的组件应该使用React.memo包装,其props应使用useMemouseCallback进行记忆化处理。
  • 所有耗时的操作应使用useMemo进行记忆化处理。
  • isPending不应该作为属性或依赖项传递给上述任何内容。

在我们的情况下,简单地包装我们的页面组件就可以了,并且它们没有任何props,所以我们可以直接渲染它们:

// ....
import { A, B, C } from "@components/Content";
const AMemo = React.memo(A);
const BMemo = React.memo(B);
const CMemo = React.memo(C);
export default function App() {
 // 代码省略
  return (
    <div className="container">
      // ...代码省略
      <div className="content">
        {tab === "A" && <AMemo />}
        {tab === "B" && <BMemo />}
        {tab === "C" && <CMemo />}
      </div>
    </div>
  );
}

天晴了雨停了你又觉得你行了,此时上面的顽疾被解决了。耗时的B页面重新渲染不再阻止阻塞页面的渲染了。

我们在之前的就聊过Memo的情况。React Memo不是你优化的第一选择。其中有一个结论是:Memo很容易被破坏,所以如果在useTransition处理过程中没很好处理Memo的话,会使我们的应用比使用useTransition之前显然更糟糕。得不偿失。

而且,要正确地进行记忆化处理实际上是相当困难的。想象一下,有如下的场景App因初始过渡而重新渲染,BMemo是否会重新渲染?

const ListMemo = React.memo(List);
const BMemo = React.memo(B);
const App = () => {
  // 如果触发startTransition,BMemo是否会重新渲染?
  const [isPending, startTransition] = useTransition();
  return (
    ...
    <BMemo>
      <ListMemo />
    </BMemo>
  )
}

答案是 - 是的,它会重新渲染。而且还是那种像吃了炫迈一样,根本停下来的那种。具体的解决方法吗,我们优先考虑下放State内容提升,在最后万不得已的情况才会考虑React.memo

从无到耗时的过渡

确保这种额外的初始重新渲染尽可能轻量的另一种方法是仅在从"无"到"非常耗时的内容"的过渡中使用useTransition。这种情况的典型示例可能是数据获取,然后将该数据放入状态中。例如:

const App = () => {
  const [data, setData] = useState();
  useEffect(() => {
    fetch('/some-url').then((result) => {
      // 大量的数据
      setData(result);
    })
  }, [])
  if (!data) return 'loading'
  return ... // 在数据可用时渲染大量数据
}

在这种情况下,如果没有数据,我们只返回一个加载状态,这不太可能很耗时。因此,如果我们将setData包装在startTransition中,由此引起的初始重新渲染不会太糟糕:它将使用空状态和加载指示器重新渲染。

更多,更详细的语法,请参看React官网 -useTransition


6. useDeferredValue

还有另一个钩子,允许我们利用并发渲染的威力:useDeferredValue。它的工作方式类似于useTransition,允许我们将某些更新标记为非关键并将它们移至后台。通常建议在没有访问状态更新函数时使用它,例如,当值来自props时。

然后,我们对上面的代码做一下改造处理:

import { useState, useDeferredValue } from "react";
// ...
import { A, B, C } from "@components/Content";
type Content = "A" | "B" | "C";
const TabContent = ({ tab }: { tab: Content }) => {
  const tabDeffered = useDeferredValue(tab);
  return (
    <>
      {tabDeffered === "A" && <A />}
      {tabDeffered === "B" && <B />}
      {tabDeffered === "C" && <C />}
    </>
  );
};
export default function App() {
  const [tab, setTab] = useState<Content>("A");
  const onBtnClick = (btn: Content) => {
    setTab(btn);
  };
  return (
    <div className="container">
      //....
      <div className="content">
        <TabContent tab={tab} />
      </div>
    </div>
  );
}

将渲染内容提取到一个组件中,并且组件接收tab作为props

然而,在这里也存在双重渲染的问题。

在页面首次渲染时,A Button是默认被选中的,我们依次点击B/C。然后下面是对应的控制台输出。

image.png

问题出现了,解决这方面的药方也有,它和解决useTransition的问题是一样的。

  • 所有受影响的内容都已进行了记忆化处理;
  • 尽量,在从"无"到"非常耗时的内容"的过渡中使用useDeferredValue

更多,更详细的语法,请参看React官网 -useDeferredValue


7. debounce VS useTransition

由于useTransition的延迟特性,有些同学就会想到,我是不是可以将其用在防抖上。当我们在输入框中快速输入内容时,我们不希望在每次输入时向后端发送请求 - 这可能会使我们的服务器崩溃。相反,我们希望引入一点延迟,以便只发送完整的文本。

通常,我们会使用类似lodash中的防抖函数(或等效函数)来实现:

或者我们可以使用在美丽的公主和它的27个React 自定义 Hook中的自定义hookuseDebounce。这不就形成了一种闭环了吗。学了,就要用上它。

function App() {
  const [valueDebounced, setValueDebounced] = useState('');
  const onChangeDebounced = debounce((e) => {
    setValueDebounced(e.target.value);
  }, 300);
  useEffect(() => {
    console.log("防抖处理的值(300ms后显示): ", valueDebounced);
  }, [valueDebounced]);
  return (
    <>
      <input type="text" onChange={onChangeDebounced} />
    </>
  );
}

这里的onChange回调被防抖处理,因此setValueDebounced只在我们停止在输入框中输入后的300毫秒后触发。

如果不使用外部库,而是使用useTransition,按照原理来讲,这是可行的。因为在过渡中设置状态是可中断的,所以我们可以利用这个特性来处理值的延迟获取。

function App() {
  const [value, setValue] = useState('');
  const [isPending, startTransition] = useTransition();
  const onChange = (e) => {
    startTransition(() => {
      setValue(e.target.value);
    });
  };
  useEffect(() => {
    console.log("transition处理的值: ", value);
  }, [value]);
  return (
    <>
      <input type="text" onChange={onChange} />
    </>
  );
}

理想很丰满,但是现实很残酷。在我们运行代码后发现,使用useTransition达不到我们的要求。在输入框中每次输入,控制台都很配合的输出对应的值。

React太快了,它能够在我们输入的这段时间内计算和提交"后台"值。

也就是说,useTransition是达不到debounce的效果。也就是实现不了防抖。

这一点,React官网也说明了这点。

image.png


后记

分享是一种态度

全文完,既然看到这里了,如果觉得不错,随手点个赞和“在看”吧。

相关文章
|
3月前
|
Python
编程之禅的奇幻之旅:探寻代码世界与生活万象的惊世共鸣,颠覆你的认知!
【8月更文挑战第7天】编程不仅是技术活,更融汇艺术与哲学。它启示我们在生活里追求简洁高效,如Python列表推导式的优雅;教会我们面对挑战时冷静分析,正如调试代码;体现分工合作的重要性,像模块化设计;并鼓励持续优化,提升效能。编程所蕴含的生活智慧,能引导我们创造更美好、有序的人生。
49 1
|
3月前
|
JavaScript 前端开发 开发者
震撼揭秘!JS模块化进化史:从混沌到秩序,一场代码世界的华丽蜕变,你怎能错过这场编程盛宴?
【8月更文挑战第23天】在 Web 前端开发领域,JavaScript 模块化已成为处理日益复杂的 Web 应用程序的关键技术。通过将代码分解成独立且可重用的模块,开发者能够更有效地组织和管理代码,避免命名冲突和依赖混乱。从最早的全局函数模式到 IIFE,再到 CommonJS 和 AMD,最终进化到了 ES6 的原生模块支持以及 UMD 的跨环境兼容性。本文通过具体示例介绍了这些模块化规范的发展历程及其在实际开发中的应用。
50 0
|
5月前
|
程序员 C++ Windows
【C++航海王:追寻罗杰的编程之路】探寻实用的调试技巧
【C++航海王:追寻罗杰的编程之路】探寻实用的调试技巧
38 0
|
6月前
|
传感器 算法 机器人
斯坦福李飞飞团队祭出“灵巧手”,泡茶剪纸炫技
【2月更文挑战第26天】斯坦福李飞飞团队祭出“灵巧手”,泡茶剪纸炫技
76 5
斯坦福李飞飞团队祭出“灵巧手”,泡茶剪纸炫技
|
6月前
|
JavaScript 前端开发 Java
二十年编程语言风云,哪款是你的爱豆?
二十年编程语言风云,哪款是你的爱豆?
|
11月前
|
资源调度 前端开发 JavaScript
useTransition真的无所不能吗?(一)
useTransition真的无所不能吗?(一)
122 0
|
Web App开发 Windows
推荐5款让你相见恨晚的神级软件,把把直击心灵
今天来给大家推荐5款良心软件,每款都是经过时间检验的精品,用起来让你的工作效率提升飞快,各个都让你觉得相见恨晚!
257 0
推荐5款让你相见恨晚的神级软件,把把直击心灵
|
JSON JavaScript 前端开发
全栈工程师的武器——MEAN
JavaScript自1995年发布以来,走过了漫长的道路。已经有了几个主要版本的ECMAScript规范,单页Web应用程序也慢慢兴起,还有支持客户端的JavaScript框架。作为一个被绝大多数浏览器支持前台脚本语言,它对浏览器的创新做出了很大的贡献。JavaScript许多很有用的特点(它是无阻塞是,它是事件驱动的,很多程序员熟悉它)可以在浏览器之外的环境中加以利用。这推动了JavaScript社区新一轮的创新,让JavaScript能在服务器和数据库中运行。
176 0
全栈工程师的武器——MEAN
|
Web App开发 程序员
启迪人心:10个有关编程的至理名言
导读:程序员世界里有哪些名言呢?Jun Auza 列出了一些启迪人心的至理名言,它们大多来自产业界富于经验的人们。 下文列出前10个供读者欣赏: 10. “People think that computer science is the art of geniuses but the actual...
1089 0
|
程序员
第12章 互联网创业神话 《丰富多彩的编程世界》
第12章 互联网创业神话 《丰富多彩的编程世界》 丰富多彩的编程世界 作者 陈光剑 第1章 名可名非常名第2章 语言的构成第3章 有趣的编程第4章 编程语言大观第5章 编程语言的组成第6章 编程范式第7章 编程思想第8章 编程心理学第9章 程序员这群人...
1005 0