别再误用useMemo了!这才是最佳实践的正确打开方式

news2024/9/29 14:51:42

useMemo是react用作性能优化的一个hook,但有一个现象,不知道的人一次不用,知道的人随时随地到处都用。本文就带你真正搞懂什么情况下可以使用useMemo。

useMemo 是一个 React Hook,它在每次重新渲染的时候能够缓存计算的结果 useMemo(calculateValue, dependencies)

import { useMemo } from 'react';

function TodoList({ todos, tab }) {
  const visibleTodos = useMemo(
    () => filterTodos(todos, tab),
      [todos, tab]
  );  
  // ...
}

在初次渲染时,useMemo 返回不带参数调用 calculateValue 的结果。
在接下来的渲染中,如果依赖项没有发生改变,它将返回上次缓存的值;否则将再次调用 calculateValue,并返回最新结果。

本文将从以下几个方面带你深入了解useMemo:

  • 使用方法
  • 如何衡量计算过程的开销是否昂贵
  • 什么情况下使用useMemo
  • 如何避免滥用useMemo
  • 常见问题
欢迎访问本人个人网站:https://www.dengzhanyong.com
关注公众号【前端筱园】,不错过每一篇推送。
加入【交流群】,共同学习成长

使用方法

默认情况下,React 会在每次重新渲染时重新运行整个组件。例如,如果 TodoList 更新了 state 或从父组件接收到新的 props,filterTodos 函数将会重新运行。

function TodoList({ todos, tab, theme }) {
  const visibleTodos = filterTodos(todos, tab);
  // ...
}

如果计算速度很快,这将不会产生问题。但是,当正在过滤转换一个大型数组,或者进行一些昂贵的计算,而数据没有改变,那么可能希望跳过这些重复计算。如果 todos 与 tab 都与上次渲染时相同,那么将计算函数包装在 useMemo 中,便可以重用已经计算过的 visibleTodos。

import { useMemo } from 'react';

function TodoList({ todos, tab, theme }) {
  const visibleTodos = useMemo(() => filterTodos(todos, tab), [todos, tab]);
  // ...
}

你需要给 useMemo 传递两样东西:
一个没有任何参数的 calculation 函数,像这样 () =>,并且返回任何你想要的计算结果。
一个由包含在你的组件中并在 calculation 中使用的所有值组成的 依赖列表。

在初次渲染时,你从 useMemo 得到的值将会是你的 calculation 函数执行的结果。

在随后的每一次渲染中,React 将会比较前后两次渲染中的 所有依赖项是否相同。如果通过 Object.is 比较所有依赖项都没有发生变化,那么 useMemo 将会返回之前已经计算过的那个值。否则,React 将会重新执行 calculation 函数并且返回一个新的值。

如何衡量计算过程的开销是否昂贵?

一般来说,除非要创建或循环遍历数千个对象,否则开销可能并不大。如果你想获得更详细的信息,可以在控制台来测量花费这上面的时间:

console.time('filter array');
const visibleTodos = filterTodos(todos, tab);
console.timeEnd('filter array');

通过打印这段计算内容所执行的时间来判断,如果执行时间>1ms,那么缓存这段计算结果就是有意义的。由于这里没有使用useMemo,所以每次重新渲染都会执行,每次渲染后看到的打印时间都差不多。
作为对比,你可以将计算过程包裹在 useMemo 中,以验证该交互的总日志时间是否减少了:

console.time('filter array');
const visibleTodos = useMemo(() => {
  return filterTodos(todos, tab); // 如果 todos 和 tab 都没有变化,那么将会跳过渲染。
}, [todos, tab]);
console.timeEnd('filter array');

这里要值得注意的是,useMemo不会让首次渲染执行的更快,所以你会看到第一次渲染时,这段内容执行的时间与上面差不多,但后续的在后续的重新渲染中,观察打印时间是否明显的减少。

什么情况下使用useMemo?

  1. useMemo中的内容计算很慢,并且依赖项很少改变

    如果每次更新,依赖值都会发生变化,这种情况下使用useMemo并不会得到明显的收益小效果。

  2. 将useMemo的计算结果作为props传递给子组件,在依赖未改变的时候,不想子组件重新渲染
    默认情况下,当一个组件重新渲染时,React 会递归地重新渲染它的所有子组件,这对于不需要太多计算来重新渲染的组件来说很好。但是如果你已经确认重新渲染很慢,你可以通过将它包装在 memo 中,这样当它的 props 跟上一次渲染相同的时候它就会跳过本次渲染:

import { memo } from 'react';
  const List = memo(function List({ items }) {
  // ...
});
export default function TodoList({ todos, tab, theme }) {
  // 每当主题发生变化时,这将是一个不同的数组……
  const visibleTodos = filterTodos(todos, tab);
  return (
    <div className={theme}>
        /* ... 所以List的props永远不会一样,每次都会重新渲染 */
        <List items={visibleTodos} />
    </div>
  );
}

在上面的示例中,filterTodos 函数总是创建一个不同数组,类似于 {} 总是创建一个新对象的方式。通常,这不是问题,但这意味着 List 属性永远不会相同,并且你的 memo 优化将不起作用。这就是 useMemo 派上用场的地方:

export default function TodoList({ todos, tab, theme }) {
// 告诉 React 在重新渲染之间缓存你的计算结果...
const visibleTodos = useMemo(
  () => filterTodos(todos, tab),
    [todos, tab] // ...所以只要这些依赖项不变...
  );
  return (
    <div className={theme}>
    /* ... List 也就会接受到相同的 props 并且会跳过重新渲染 */
    <List items={visibleTodos} />
    </div>
  );
}

还有另外一种方式,你可以将 JSX 节点本身包裹在 useMemo 中,而不是将 List 包裹在 memo 中:

export default function TodoList({ todos, tab, theme }) {
  const visibleTodos = useMemo(() => filterTodos(todos, tab), [todos, tab]);
  const children = useMemo(() => <List items={visibleTodos} />, [visibleTodos]);
  return (
    <div className={theme}>
       {children}
    </div>
  );
}

他们的行为表现是一致的。如果 visibleTodos 没有改变,List 将不会重新渲染。
手动将 JSX 节点包裹到 useMemo 中并不方便,比如你不能在条件语句中这样做。所以通常会选择使用 memo 包装组件而不是使用 useMemo 包装 JSX 节点。
3. 传递的值稍后用作某些 Hook 的依赖项。例如,也许另一个 useMemo 计算值依赖它,或者 useEffect 依赖这个值

如何避免滥用useMemo?

  1. 减少不必要的依赖项
  2. 避免不必要的更新state的effect,有两种不必使用 Effect 的常见情况:
    (1)你不必使用 Effect 来转换渲染所需的数据。
    假设你有一个包含了两个 state 变量的组件:firstName 和 lastName。你想通过把它们联结起来计算出 fullName。此外,每当 firstName 和 lastName 变化时,你希望 fullName 都能更新。你的第一直觉可能是添加一个 state 变量:fullName,并在一个 Effect 中更新它:
function Form() {
  const [firstName, setFirstName] = useState('Taylor');
  const [lastName, setLastName] = useState('Swift');
  
  
  // 🔴 避免:多余的 state 和不必要的 Effect
  const [fullName, setFullName] = useState('');
  useEffect(() => {
    setFullName(firstName + ' ' + lastName);
  }, [firstName, lastName]);
  // ...
}

大可不必这么复杂。而且这样效率也不高:它先是用 fullName 的旧值执行了整个渲染流程,然后立即使用更新后的值又重新渲染了一遍。让我们移除 state 变量和 Effect:

function Form() {
  const [firstName, setFirstName] = useState('Taylor');
  const [lastName, setLastName] = useState('Swift');
  // ✅ 非常好:在渲染期间进行计算
  const fullName = firstName + ' ' + lastName;
  // ...
}

如果一个值可以基于现有的 props 或 state 计算得出,不要把它作为一个 state,而是在渲染期间直接计算这个值。这将使你的代码更快(避免了多余的 “级联” 更新)、更简洁(移除了一些代码)以及更少出错。
(2)你不必使用 Effect 来处理用户事件。例如,你想在用户购买一个产品时发送一个 /api/buy 的 POST 请求并展示一个提示。在这个购买按钮的点击事件处理函数中,你确切地知道会发生什么。但是当一个 Effect 运行时,你却不知道用户做了什么(例如,点击了哪个按钮)。这就是为什么你通常应该在相应的事件处理函数中处理用户事件。

  1. 非必要不进行状态提升
    如果只是当前组件使用某个state,则不要将这个state提升到父组件去定义,然后通过props传递下来
  2. 使用打印日志或一些性能工具手段,来判断是否需要使用useMemo来进行缓存

在其他情况下,将计算过程包装在 useMemo 中没有任何好处。不过这样做也没有重大危害,所以很多人选择不考虑具体情况,尽可能多地使用 useMemo。不过这种做法会降低代码可读性,并且没有任何效果。

常见问题

  1. 依赖值为对象时,useMemo每次都会重新计算

假设你有一个计算函数依赖于直接在组件主体中创建的对象:

function Dropdown({ allItems, text }) {
const searchOptions = { matchMode: 'whole-word', text };


const visibleItems = useMemo(() => {
  return searchItems(allItems, searchOptions);
}, [allItems, searchOptions]); // 🚩 提醒:依赖于在组件主体中创建的对象
// ...

依赖这样的对象会破坏记忆化。当组件重新渲染时,组件主体内的所有代码都会再次运行。创建 searchOptions 对象的代码行也将在每次重新渲染时运行。因为 searchOptions 是你的 useMemo 调用的依赖项,而且每次都不一样,React 知道依赖项是不同的,并且每次都重新计算 searchItems。
要解决此问题,你可以在将其作为依赖项传递之前记忆 searchOptions 对象 本身:

function Dropdown({ allItems, text }) {
const searchOptions = useMemo(() => {
  return { matchMode: 'whole-word', text };
}, [text]); // ✅ 只有当 text 改变时才会发生改变


const visibleItems = useMemo(() => {
  return searchItems(allItems, searchOptions);
}, [allItems, searchOptions]); // ✅ 只有当 allItems 或 serachOptions 改变时才会发生改变
// ...

在上面的例子中,如果 text 没有改变,searchOptions 对象也不会改变。然而,更好的解决方法是将 searchOptions 对象声明移到 useMemo 计算函数的内部:

function Dropdown({ allItems, text }) {
	const visibleItems = useMemo(() => {
		const searchOptions = { matchMode: 'whole-word', text };
		return searchItems(allItems, searchOptions);
	  }, [allItems, text]); // ✅ 只有当 allItems 或者 text 改变的时候才会重新计算
	// ...

这里依赖的是一个字符串类型的值,只有当值确实发生改变时才会重新执行
2. 未设置依赖数组时,useMemo每次都会重新计算
确保你已将依赖项数组指定为第二个参数!如果你忘记了依赖数组,useMemo 将每次重新运行计算:

function TodoList({ todos, tab }) {
// 🔴 每次都重新计算:没有依赖数组
const visibleTodos = useMemo(() => filterTodos(todos, tab));
// ...
})
  1. 不允许在循环中调用useMemo
    假设 Chart 组件被包裹在 memo 中。当 ReportList 组件重新渲染时,你想跳过重新渲染列表中的每个 Chart。但是,你不能在循环中调用 useMemo:
function ReportList({ items }) {
return (
<article>
      {items.map(item => {
        // 🔴 你不能像这样在循环中调用 useMemo:
        const data = useMemo(() => calculateReport(item), [item]);
        return (
          <figure key={item.id}>
            <Chart data={data} />
          </figure>
        );
      })}
</article>
  );
}

相反,为每个 item 提取一个组件并为单个 item 记忆数据:

function ReportList({ items }) {
return (
    <article>
      {items.map(item =>
        <Report key={item.id} item={item} />
      )}
    </article>
  );
}


function Report({ item }) {
// ✅ 在顶层调用 useMemo:
const data = useMemo(() => calculateReport(item), [item]);
return (
    <figure>
      <Chart data={data} />
    </figure>
  );
}

或者,你可以删除 useMemo 并将 Report 本身包装在 memo 中。如果 item props 没有改变,Report 将跳过重新渲染,因此 Chart 也会跳过重新渲染:

function ReportList({ items }) {
// ...
}


const Report = memo(function Report({ item }) {
const data = calculateReport(item);
return (
    <figure>
      <Chart data={data} />
    </figure>
  );
});

通过本文的学习,相信你应该真正的搞懂了useMemo,拒绝滥用useMemo

写在最后

欢迎加入前端筱园交流群:点击加入交流群
关注我的公众号【前端筱园】,不错过每一篇推送

描述文字

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.coloradmin.cn/o/2177066.html

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈,一经查实,立即删除!

相关文章

【py】字符串切片

下面是一个简单的Python脚本&#xff0c;它读取输入的学号和姓名&#xff0c;然后按照要求拆分并输出&#xff1a; # 从键盘输入学号和姓名 input_str input("请输入学号和姓名&#xff1a;") # 学号和姓名的长度&#xff0c;可以根据实际情况调整 grade_length …

Linux下V4L2实时显示摄像头捕捉画面(完整QT+C++代码)

目录 一、V4L2 1、简介 2、编程与应用 二、示例演示 1、例子说明&#xff1a; 2、关键的代码演示 3、完整的例子的代码 一、V4L2 1、简介 V4L2&#xff0c;即Video for Linux Two&#xff0c;是Linux下关于视频设备的内核驱动框架&#xff0c;为驱动和应用程序提供了一…

前端vue-form表单的验证

form表单验证的完整步骤

Nginx反向代理配置支持websocket

一、官方文档 WebSocket proxying 为了将客户端和服务器之间的连接从HTTP/1.1转换为WebSocket&#xff0c;使用了HTTP/1.1中可用的协议切换机制&#xff08;RFC 2616: Hypertext Transfer Protocol – HTTP/1.1&#xff09;。 然而&#xff0c;这里有一个微妙之处:由于“升级”…

TLS详解

什么是TLS TLS(Transport Layer Security)传输层安全性协议 &#xff0c;它的前身是SSL(Secure Sockets Layer)安全套接层&#xff0c;是一个被应用程序用来在网络中安全的通讯协议&#xff0c; 防止电子邮件、网页、消息以及其他协议被篡改或是窃听。是用来替代SSL的&#xf…

ONFI 5.1:定义、缩写语和约定

address 该地址由一个行地址和一个列地址组成。行地址标识要访问的page、block和LUN。列地址标识要访问的page中的byte或word。 asynchronous 异步是指数据用WE_n信号进行写&#xff0c;RE_n信号进行读。 block 由多个page组成&#xff0c;是擦除操作的最小可寻址单元。 column…

稀土阻燃协效剂-磷氮系的应用

稀土阻燃协效剂凭借独特的稀土4f电子层结构,在聚合物材料燃烧时可催化酯化成炭,迅速在高分子表面形成致密连续的碳层,隔绝聚合物材料内部的可燃性气体与氧气的接触,从而达到阻燃抑烟的效果,且燃烧时不产生有毒有害气体。 金士镧系列稀土阻燃剂是一种基于稀土协效阻燃的复合阻燃…

Windows11如何关闭“显示更多选项”,直接展示所有选项

在windows11系统中&#xff0c;右击&#xff0c;会有“显示更多选项”&#xff0c;每次都要点一下这个按钮&#xff0c;才能看到所有的选项&#xff0c;太麻烦&#xff0c;那么有什么办法去掉呢&#xff1f; 1、以管理员的方式打开命令提示符 winR&#xff1b;cmd回车 执行命…

在Ubuntu和centos系统安装JDK教程

目录 1. 先使用apt 查看有哪些软件包2.使用apt安装软件包3.确认是否安装4.centos安装jdk Linux会把一些软件包&#xff0c;放到对应的服务器上&#xff0c;通过包管理器这样的程序&#xff0c;来把这些软件包下载安装 包管理器&#xff1a; Ubuntu&#xff1a;apt centos&#…

工程设备包括哪些内容?

工程设备是构成或计划构成永久工程一部分的机电设备、金属结构设备&#xff0c;仪器装置及其他类似的设备和装置。它们在工程建设中扮演着至关重要的角色&#xff0c;涵盖了从基础建设到设备安装的多个方面。以下是整理出来的一些工程设备主要的内容&#xff1a; 1. 建筑机械设…

实用好软-----电脑端 从视频中导出音频的方便工具

最近想从一个视频中导出个音乐&#xff0c;百度找很多没有合适的工具。最终找到了一款很方便 而且操作超级简单的工具。打开这个工具后只需要把需要导出音乐的视频拖进窗口里就会自动导出音乐mp3。方便小巧&#xff0c;而且音频效果还是不错的。 一些视频转换成音频文件&#x…

从零开始学习OMNeT++系列第一弹——OMNeT++的介绍与安装

最近由于由于工作上的需求&#xff0c;接了一个网络仿真的任务。于是开始调研各个仿真平台&#xff0c;然后根据目前的需求和网络上公开资料的多少&#xff0c;决定使用omnet这个网络仿真平台。现在也是刚开始学习&#xff0c;所以决定记录一下从零开始的这个学习过程。因为虽然…

社交应用性能提升秘籍:推拉结合优化方案全解读!

我是小米,一个喜欢分享技术的29岁程序员。如果你喜欢我的文章,欢迎关注我的微信公众号“软件求生”,获取更多技术干货! Hello,大家好!我是你们的老朋友小米,一个积极活泼的29岁技术分享达人~ 今天要跟大家分享的是我最近在个人项目里遇到的一个有趣的优化案例——“推拉…

OptiTrack与Xsens光、惯动捕中用于动画制作的尖端设备对比

随着动画、电影、游戏等数字内容行业的迅速发展&#xff0c;捕捉演员的动作并将其转化为虚拟角色的技术越来越受到重视。两种主要的动作捕捉技术——光学捕捉系统和惯性动作捕捉系统——代表了当前市场的最前沿。本文将对比两种技术的代表性设备&#xff1a;OptiTrack的光学动作…

Vue3 动态加载图片不显示问题

一、图片目录结构 二、批量导出图片 exportImage.ts const images import.meta.glob(/assets/icons/*.{png,jpg,jpeg,svg}, { eager: true });const imageMap Object.entries(images).reduce((acc, [key, value]) > {const imageName key.split(/).pop().replace(/\.…

PDF对话RAG应用开发实战

与大型 PDF 对话很酷。你可以与笔记、书籍和文档等聊天。这篇博文将帮助你构建一个基于 Multi RAG Streamlit 的 Web 应用程序&#xff0c;以通过对话式 AI 聊天机器人读取、处理和与 PDF 数据交互。以下是此应用程序工作原理的分步分解&#xff0c;使用简单的语言易于理解。 N…

运用电磁铁需求注意哪些问题

电磁铁是比较常用的磁场设备&#xff0c;在与磁性研究相关的实验室里&#xff0c;我们能经常看到&#xff0c;那磁场设备在使用的时候&#xff0c;包括各类电磁铁、亥姆霍兹线圈、螺线管等&#xff0c;有什么需要注意的事项呢&#xff1f; 电磁铁设备主要包括电磁铁以及配套电…

大跳水!华为三折叠手机黄牛价暴跌,市场需求显真相

华为首款三折叠手机Mate XT上市初期受到黄牛热炒&#xff0c;但由于实际需求不足和定价过高&#xff0c;市场溢价迅速下跌&#xff0c;反映出折叠屏手机市场仍需培养消费者接受度。 转载&#xff1a;科技新知 原创 作者丨依蔓 编辑丨蕨影 惊了&#xff01;华为首款三折叠手机M…

Electron 主进程与渲染进程、预加载preload.js

在 Electron 中&#xff0c;主要控制两类进程&#xff1a; 主进程 、 渲染进程 。 Electron 应⽤的结构如下图&#xff1a; 如果需要更深入的了解electron进程&#xff0c;可以访问官网 流程模型 文档。 主进程 每个 Electron 应用都有一个单一的主进程&#xff0c;作为应用…

如何通过VSM识别生产过程中的信息流浪费?

VSM&#xff08;价值流图&#xff09;&#xff0c;作为精益生产的核心工具之一&#xff0c;它不仅仅是一张简单的流程图&#xff0c;更是企业优化生产流程、提升价值传递效率的指南针。通过VSM&#xff0c;我们可以清晰地看到从原材料到成品交付的全过程&#xff0c;包括物料流…