首页 > 其他分享 >React中何时使用memo、useCallback、useMemo以及useRef进行性能优化

React中何时使用memo、useCallback、useMemo以及useRef进行性能优化

时间:2024-05-28 18:26:32浏览次数:10  
标签:useRef 缓存 useMemo memo useCallback props 组件

react 无法做到像 vue 一样自动收集依赖更新(期待 react19 的 React Compiler),需要开发人员手动的进行性能优化,此时 memouseCallbackuseMemouseRef 就是性能优化中的重要 API

本文虽然介绍可应用场景,但是正常开发中,尤其是 useCallback
除非遇到性能问题或者组件库封装,亦或是能力足够,否则不建议轻易各种 useCallback,迭代过程中很可能出现屎山

memo

正常情况下,父组件发生变化时,就算变化的 state 与子组件无关,但还是会导致子组件 rerender。
这种情况下,可以使用 memo 包裹子组件

memo 的作用:被 memo 包裹的组件,会自动对 props 进行浅比较,若传入的 props 没有改变,则不会重新 render

简单场景

代码示例:

image

效果图:

image

图中可以看到,虽然 Child 子组件的 name 没有发生任何变化,但是由于父组件的 state 改变导致整个组件重新渲染,子组件也无法避免 rerender(第一个打印是初始加载时的渲染)

解决方法

给子组件进行 memo 包裹,使其只有 props 相关发生改变时才重渲染

代码示例:

image

效果图:

image

子组件 memo 后,props 只要不发生变化就不会重渲染

引用数据类型的 props 导致重渲染

以上示例中,我们给子组件传入的 name 是基本数据类型,如果传入一个 obj 复杂数据类型,虽然值没发生变化,但是子组件依旧发生了重渲染

<Child name="张三" obj={{ a: 1 }} />

效果图:

image

图中结合代码可见,obj 是传入的不变值,看似 props 是没有发生变化的。
但是:obj 是引用数据类型,其数据是存到堆内存中的,和基本类型不同state 每发生一次变化,obj 的内存地址就会重新变动,生成的是一个全新的 obj 对象,这就导致了表面上看似 props 没变化,实际上是 obj 是一直在变,一直在 rerender

解决方法一:数据提取至外部

将静态不变的数据提取到组件外,组件重渲染时,由于对象是在组件外的,不会触发更新

const obj = {
  a: 1,
};

function App() {
  return (
    // ....
    <Child name="张三" obj={obj} />
  );
}

解决方法二:useMemo

使用 useMemo 缓存,和 useEffect 用法相似,不过第一个函数需要返回数据,第二个参数是依赖,空数组就是仅初始化执行,但是 useMemo 大部分是用于计算缓存的,纯静态值不推荐

image

<Child
  name="张三"
  obj={useMemo(() => {
    return {
      a: 1,
    };
  }, [])}
/>

解决方法三:memo 手动深度对比

memo 有第二个参数,是一个函数,函数第一个参数是更新前的 props,第二个参数是更新后的 props,可以自行对比,返回 true 不更新,返回 false 说明两次 props 不一致,更新。

image

深度对比的第三方库很多,此处以 fast-deep-equal 为例

// 父组件不变
<Child name="张三" obj={{ a: 1 }} />

// 子组件 memo 参数深度比较
import isDeep from "fast-deep-equal";

const Child: React.FC<ChildProps> = memo((props) => {
    // ....
}, isDeep);

useMemo

useMemo 通常用来缓存不常变动的大量的逻辑计算结果,就像上文中,使用 useMemo 缓存了 obj 对象,其实就可以把 obj 当作很复杂的处理后的一个结果,但是静态数据提取至外部更简单。
可以把 useMemo 理解为 vue 中的 computed 计算属性

简单场景

示例代码:

image

当我更改时间戳时,computedCounttimestamp 半毛钱关系没有,但依旧每次都会重新执行 computedCount 函数,每次执行函数的计算花费 50-60 毫秒不等,如果计算内容再复杂一点,每次都会产生大量无用开支

image

解决方法

使用 useMemo 进行计算结果缓存。

代码示例:

image

效果图:

image

useCallback

当父组件给子组件传递函数时,父组件状态更改,会导致子组件 rerender

简单场景

代码示例:

image

更改父组件的 timestamp,其中 getList 函数跟 timestamp 半毛钱关系没有,就算子组件加了 memo 和 深对比,也无法阻止 rerender

image

原因:

  • 函数无法进行对比,总不能 JSON.stringify 对比代码内容吧。
  • timestamp 更改,导致组件重新渲染,getList 函数的内存地址重新创建,memo 无法对比,所以重新 rerender

解决方法

使用 useCallback 缓存 getList 函数
不要依赖项无脑写空,如果函数内部用到了某个 state,必须写入依赖项,否则拿不到最新值

代码示例:

image

效果图:

image

useRef

useRef 通常第一认知是用于获取 dom 元素,实际它也可以用来进行变量记录,和 useState 不同的是,useRef 记录的变量更改时是不会刷新视图的,也就是非响应式数据。

何时使用 useRef 而不是 useState
当一个数据不需要展示到页面上,仅仅作为一个记录值,比如分页数据。
请求后端数据时,页码和分页尺寸通常是不会显示到页面上的(纯受控分页除外),当页码改变,

使用 useState 的效果

image

image

这种写法,由于 setState 为异步,需要在 useEffect 中拿到页码改变后的最新值并请求(也可以有其它方式,暂不赘述),而且每次页码 +1,都会导致组件 rerender,这也是一部分无用的性能开销,重要的是 rerender

使用 useRef 的效果

useRef 最重要的就是不会导致组件 reredner

image

image

记录了页码的变动,也没有导致组件的 rerender

使用场景总结

  • memo
    • 子组件接收了父组件的 props,通常都推荐向子组件添加 memo,毕竟 rerender 肯定比对比消耗高
  • useMemo
    • 当出现复杂大量的计算结果直接显示到视图上时,通常需要使用 useMemo 进行计算结果缓存,小量的计算结果没必要,因为缓存本身是需要消耗性能的,普通计算缓存,可能缓存本身都比计算的性能开支高
  • useCallback
    • 主要应用于 父组件的函数传递给子组件 的情况下,该函数可以使用 useCallback 缓存,同时 useCallback 也可以解决 react 闭包问题,当然这个不是本篇的重点讨论范围
  • useRef
    • 通常用于数据的更改记录,这些数据往往都不会参与视图的渲染,仅仅是代码内部消化的一些数据

无论是哪种优化方式,都需要考虑到缓存开销,过分的优化可能适得其反,如果不是遇到性能瓶颈,不是特别推荐去使用 useCallback

标签:useRef,缓存,useMemo,memo,useCallback,props,组件
From: https://www.cnblogs.com/jsonq/p/18217627

相关文章