react 无法做到像 vue 一样自动收集依赖更新(期待 react19 的 React Compiler),需要开发人员手动的进行性能优化,此时 memo
、useCallback
、useMemo
、useRef
就是性能优化中的重要 API
本文虽然介绍可应用场景,但是正常开发中,尤其是
useCallback
。
除非遇到性能问题或者组件库封装,亦或是能力足够,否则不建议轻易各种useCallback
,迭代过程中很可能出现屎山
memo
正常情况下,父组件发生变化时,就算变化的 state 与子组件无关,但还是会导致子组件 rerender。
这种情况下,可以使用 memo
包裹子组件
memo
的作用:被 memo 包裹的组件,会自动对 props 进行浅比较,若传入的 props 没有改变,则不会重新 render
简单场景
代码示例:
效果图:
图中可以看到,虽然 Child
子组件的 name 没有发生任何变化,但是由于父组件的 state 改变导致整个组件重新渲染,子组件也无法避免 rerender(第一个打印是初始加载时的渲染)
解决方法
给子组件进行 memo
包裹,使其只有 props 相关发生改变时才重渲染
代码示例:
效果图:
子组件 memo
后,props 只要不发生变化就不会重渲染
引用数据类型的 props 导致重渲染
以上示例中,我们给子组件传入的 name
是基本数据类型,如果传入一个 obj 复杂数据类型,虽然值没发生变化,但是子组件依旧发生了重渲染
<Child name="张三" obj={{ a: 1 }} />
效果图:
图中结合代码可见,obj
是传入的不变值,看似 props 是没有发生变化的。
但是:obj
是引用数据类型,其数据是存到堆内存中的,和基本类型不同,state
每发生一次变化,obj
的内存地址就会重新变动,生成的是一个全新的 obj
对象,这就导致了表面上看似 props 没变化,实际上是 obj
是一直在变,一直在 rerender
解决方法一:数据提取至外部
将静态不变的数据提取到组件外,组件重渲染时,由于对象是在组件外的,不会触发更新
const obj = {
a: 1,
};
function App() {
return (
// ....
<Child name="张三" obj={obj} />
);
}
解决方法二:useMemo
使用 useMemo
缓存,和 useEffect
用法相似,不过第一个函数需要返回数据,第二个参数是依赖,空数组就是仅初始化执行,但是 useMemo
大部分是用于计算缓存的,纯静态值不推荐
<Child
name="张三"
obj={useMemo(() => {
return {
a: 1,
};
}, [])}
/>
解决方法三:memo 手动深度对比
memo 有第二个参数,是一个函数,函数第一个参数是更新前的 props,第二个参数是更新后的 props,可以自行对比,返回 true 不更新,返回 false 说明两次 props 不一致,更新。
深度对比的第三方库很多,此处以 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
计算属性
简单场景
示例代码:
当我更改时间戳时,computedCount
跟 timestamp
半毛钱关系没有,但依旧每次都会重新执行 computedCount
函数,每次执行函数的计算花费 50-60 毫秒不等,如果计算内容再复杂一点,每次都会产生大量无用开支
解决方法
使用 useMemo
进行计算结果缓存。
代码示例:
效果图:
useCallback
当父组件给子组件传递函数时,父组件状态更改,会导致子组件 rerender
简单场景
代码示例:
更改父组件的 timestamp
,其中 getList
函数跟 timestamp
半毛钱关系没有,就算子组件加了 memo
和 深对比,也无法阻止 rerender
原因:
- 函数无法进行对比,总不能
JSON.stringify
对比代码内容吧。 timestamp
更改,导致组件重新渲染,getList
函数的内存地址重新创建,memo 无法对比,所以重新 rerender
解决方法
使用 useCallback
缓存 getList
函数
不要依赖项无脑写空,如果函数内部用到了某个 state,必须写入依赖项,否则拿不到最新值
代码示例:
效果图:
useRef
useRef
通常第一认知是用于获取 dom 元素,实际它也可以用来进行变量记录,和 useState
不同的是,useRef
记录的变量更改时是不会刷新视图的,也就是非响应式数据。
何时使用 useRef
而不是 useState
?
当一个数据不需要展示到页面上,仅仅作为一个记录值,比如分页数据。
请求后端数据时,页码和分页尺寸通常是不会显示到页面上的(纯受控分页除外),当页码改变,
使用 useState 的效果
这种写法,由于 setState
为异步,需要在 useEffect
中拿到页码改变后的最新值并请求(也可以有其它方式,暂不赘述),而且每次页码 +1,都会导致组件 rerender,这也是一部分无用的性能开销,重要的是 rerender
使用 useRef 的效果
useRef
最重要的就是不会导致组件 reredner
记录了页码的变动,也没有导致组件的 rerender
使用场景总结
- memo
- 子组件接收了父组件的 props,通常都推荐向子组件添加
memo
,毕竟 rerender 肯定比对比消耗高
- 子组件接收了父组件的 props,通常都推荐向子组件添加
- useMemo
- 当出现复杂大量的计算结果直接显示到视图上时,通常需要使用
useMemo
进行计算结果缓存,小量的计算结果没必要,因为缓存本身是需要消耗性能的,普通计算缓存,可能缓存本身都比计算的性能开支高
- 当出现复杂大量的计算结果直接显示到视图上时,通常需要使用
- useCallback
- 主要应用于 父组件的函数传递给子组件 的情况下,该函数可以使用
useCallback
缓存,同时useCallback
也可以解决 react 闭包问题,当然这个不是本篇的重点讨论范围
- 主要应用于 父组件的函数传递给子组件 的情况下,该函数可以使用
- useRef
- 通常用于数据的更改记录,这些数据往往都不会参与视图的渲染,仅仅是代码内部消化的一些数据
标签:useRef,缓存,useMemo,memo,useCallback,props,组件 From: https://www.cnblogs.com/jsonq/p/18217627无论是哪种优化方式,都需要考虑到缓存开销,过分的优化可能适得其反,如果不是遇到性能瓶颈,不是特别推荐去使用
useCallback