react 无法做到像 vue 一样自动收集依赖更新~~(期待 react19 的 React Compiler)~~,React 19 已正式发布,但有过多的 breakchange,建议观望等生态兼容,可使用 react-compiler-runtime 作为 react19 以下的 compiler polyfill 体验。
开发过程中需要开发人员手动的进行性能优化,此时 memo
、useCallback
、useMemo
、useRef
就是性能优化中的重要 API(其实就是缓存,减少 rerender 而已)
useCallback
:除非遇到性能问题或者组件封装,亦或是能力足够抹平差异,否则不建议到处使用,很可能导致问题排查困难useMemo
:同样作为缓存 API,React 官方也是推荐在昂贵的计算下使用(更多使用场景建议去读一下官方文档)正常情况下,父组件发生变化时,就算变化的 state 与子组件无关,但还是会导致子组件 rerender。这种情况下,可以使用 memo
包裹子组件
memo
的作用:被 memo 包裹的组件,会自动对 props 进行浅比较,若传入的 props 没有改变,则不会重新 render
代码示例:
效果图:
图中可以看到,虽然 Child
子组件的 name 没有发生任何变化,但是由于父组件的 state 改变导致整个组件重新渲染,子组件也无法避免 rerender(第一个打印是初始加载时的渲染)
给子组件进行 memo
包裹,使其只有 props 相关发生改变时才重渲染
代码示例:
效果图:
子组件 memo
后,props 只要不发生变化就不会重渲染
以上示例中,我们给子组件传入的 name
是基本数据类型,如果传入一个 obj 复杂数据类型,虽然值没发生变化,但是子组件依旧发生了重渲染
<Child name="张三" obj={{ a: 1 }} />
效果图:
图中结合代码可见,obj
是传入的不变值,看似 props 是没有发生变化的。但是:obj
是引用数据类型,其数据是存到堆内存中的,和基本类型不同,state
每发生一次变化,obj
的内存地址就会重新变动,生成的是一个全新的 obj
对象,这就导致了表面上看似 props 没变化,实际上是 obj
是一直在变,一直在 rerender
将静态不变的数据提取到组件外,组件重渲染时,由于对象是在组件外的,不会触发更新,若数据依赖了 state 等组件内数据,推荐第三种解决方法
const obj = {
a: 1,
};
function App() {
return (
// ....
<Child name="张三" obj={obj} />
);
}
使用 useMemo
缓存,和 useEffect
用法相似,不过第一个函数需要返回数据,第二个参数是依赖,空数组就是仅初始化执行,但是 useMemo
大部分是用于计算缓存的,纯静态值不推荐
<Child
name="张三"
obj={useMemo(() => {
return {
a: 1,
};
}, [])}
/>
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
缓存了 obj 对象,其实就可以把 obj 当作很复杂的处理后的一个结果,但是静态数据提取至外部更简单。可以把 useMemo
理解为 vue 中的 computed
计算属性
示例代码:
当我更改时间戳时,computedCount
跟 timestamp
半毛钱关系没有,但依旧每次都会重新执行 computedCount
函数,每次执行函数的计算花费 50-60 毫秒不等,如果计算内容再复杂一点,每次都会产生大量无用开支
使用 useMemo
进行计算结果缓存。
代码示例:
效果图:
当父组件给子组件传递函数时,父组件状态更改,会导致子组件 rerender
代码示例:
更改父组件的 timestamp
,其中 getList
函数跟 timestamp
半毛钱关系没有,就算子组件加了 memo
和 深对比,也无法阻止 rerender
原因:
JSON.stringify
对比代码内容吧。timestamp
更改,导致组件重新渲染,getList
函数的内存地址重新创建,memo 无法对比,所以重新 rerender使用 useCallback
缓存 getList
函数 不要依赖项无脑写空,如果函数内部用到了某个 state,必须写入依赖项,否则拿不到最新值
代码示例:
效果图:
useRef
第一认知是用于获取 dom 元素,但 useRef
也具有记忆功能, 可以用来进行变量记录。和 useState
不同的区别是:
useRef
记录的变量更改时是不会刷新视图的,也就是非响应式数据。既然是非响应式,那和常量的区别是:
useRef
记忆的不会何时使用 useRef
而不是 useState
?这是 react 中文网对 useRef
的描述
当一个数据不需要展示到页面上,仅仅作为一个记录值,比如分页数据。请求后端数据时,页码和分页尺寸通常是不会显示到页面上的(纯受控分页除外),当页码改变,去请求后端数据
开发过程中,页码和页数使用
useRef
是非常常见的方式,不显示到视图的数据优先用useRef
,不仅使用比useEffect
方便 ,且可以减少 rerender
这种写法,由于 setState
为异步,需要在 useEffect
中拿到页码改变后的最新值并请求(也可以有其它方式,暂不赘述),而且每次页码 +1,都会导致组件 rerender,这也是一部分无用的性能开销,重要的是 rerender
useRef
最重要的就是不会导致组件 reredner
记录了页码的变动,也没有导致组件的 rerender
memo
,props 不变化,就会复用上一次的渲染useMemo
进行计算结果缓存,小量的计算结果没必要,因为缓存本身是需要消耗性能的,普通计算若缓存,可能缓存本身都比计算的性能开支高useCallback
缓存,同时 useCallback
也可以解决 react 闭包问题,当然这个不是本篇的重点讨论范围useCallback
的看法就是,需要在合适的场景使用,滥用不可取,很可能会本末倒置,若使用不熟练,大概率会造成一系列问题。useRef
往往可以减少组件的 rerender除
useRef
外,memo
useMemo
useCallback
都需要考虑到缓存开销,过分的滥用优化可能适得其反,其中,useCallback
需格外注意其使用场景和使用方式,避免造成不可维护代码