memo、useMemo、useCallback到底怎么用呢
最近维护一些老项目,我发现memo
、useMemo
以及useCallback
这些React Hooks被广泛但不加区分地使用,“四处横飞”。虽然从性能优化的角度看,它们通过缓存结果或函数的确减少了不必要的计算,从而提升了效率,但过度或不当的使用显著降低了代码的可读性和可维护性。有些文件中充斥着大量的useCallback
实例,每个Hook的意图变得模糊不清:一处可能依赖于A并返回B,另一处则可能是C依赖于B。这对于新加入的开发者来说,几乎等同于代码层面的“迷宫”,极大增加了维护工作的复杂度。基于此,我梳理了一下 memo
、useMemo
以及useCallback
不同场景下的使用问题。
不使用 memo
// AppNoMemo.tsx
const AppNoMemo = () => {
console.log('no memo')
return ( <div>no memo</div> )
}
export default AppNoMemo
父组件 state
修改,父组件会重新渲染,从而导致子组件重新执行渲染。
使用 memo
import { memo } from 'react'
const AppWithMemo = () => {
console.log('memo')
return (
<div>width memo</div>
)
}
export default memo(AppWithMemo)
使用了 memo
, 当父组件 state
修改,并不会导致子组件重新执行。我们可以 memo
缓存了组件。
使用 memo 且存在 props 的情况
props 简单类型
import { memo } from 'react'
const AppWithMemo = (props) => {
console.log('props')
return (
<div>props memo, {props.data}</div>
)
}
export default memo(AppWithMemo)
memo
本身会通过 Object.is 比较组件中的每个 prop
与其先前的值。注意,Object.is(3, 3)
为 true
,但 Object.is({}, {})
为 false
。所以,prop
是简单类型的话,memo
的缓存组件作用是生效的。
props 非简单类型
import { memo } from 'react'
const ObjPropMemo = (props: any) => {
console.log('obj')
return (
<div>props memo, {props.data?.name}</div>
)
}
export default memo(ObjPropMemo)
props
是非简单类型,比如对象,比如函数。这个时候即使使用了 memo
, 组件也不会被缓存,哪怕 props 并没有变化,父组件的重新渲染也会导致子组件重新渲染。
解决方案
方案一:最小化 props
的变化
确保组件在其 props
中接受必要的最小信息。例如,它可以接受单独的值而不是整个对象:
function Page() {
const [name, setName] = useState('Taylor');
const [age, setAge] = useState(42);
return <Profile name={name} age={age} />;
}
const Profile = memo(function Profile({ name, age }) {
// ...
});
方案二: 保证新的 prop
旧 prop
引用相同
我们可以直接使用同一个变量,这个变量可以是函数外定义的同一个变量或者使用 useState\useMemo\useCallback
缓存的变量。
const MemoProfile = memo(function Profile({data }: any) {
console.log('render')
const {name, age} = data
return <>
<div>{name}</div>
<div>{age}</div>
</>
});
// 使用同一个变量,保证保证新的prop旧prop引用相同
const profileInfo = {
name: 'Amanda',
age: 99
}
function Page() {
return <>
<MemoProfile data={profileInfo} />
</>;
}
使用 useState
:
const MemoProfile = memo(function Profile({data }: any) {
console.log('render')
const {name, age} = data
return <>
<div>{name}</div>
<div>{age}</div>
</>
});
function Page() {
const [profileInfo, setrofileInfo] = useState({
name: 'Amanda',
age: 99
});
const [count, setCount] = useState(0);
return <>
<MemoProfile data={profileInfo} />
</>;
}
当然也可以使用 useMemo\useCallback
,useMemo
用来缓存值,useCallback
用来缓存函数。
const cachedValue = useMemo(()=>{
...
return calculateValue;
}, dependencies)
只有 dependencies 变化的时候才会重新计算值,传给子组件的 props 可以使用 useMemo
返回的值,配合 memo
达到缓存整个子组件的效果。但是,并不是说给子组件的 props 必须使用 useMemo
来封装一下,如上所述,我们也可以使用 useState 或者函数组件外部定义的对象,来保证新的 prop
旧 prop
引用相同。在对象有明显依赖关系的时候,我们使用 useMemo
可以提高代码的可读性。
const cachedFn = useCallback(fn, dependencies)
使用 useCallback 缓存函数,同样的,不是说给子组件的函数必须使用 useCallback
封装一下,如果一个函数没有依赖项,我们完全可以使用独立函数传给子组件。
const MemoProfile = memo(function Profile({data, sayHello }: any) {
console.log('render')
const {name, age} = data
return <>
<div onClick={sayHello}>{name}</div>
<div>{age}</div>
</>
});
const sayHello = () => {
console.log('sayHello')
}
function Page() {
const [profileInfo, setrofileInfo] = useState({
name: 'Amanda',
age: 99
});
return <>
<MemoProfile data={profileInfo} sayHello={sayHello}/>
</>;
}
总结
我们在使用 memo
缓存子组件的时候,传递给子组件的 props
引用需要相同,否则 memo 不起作用。同时,传给子组件的 props
不必非得使用 useMemo
或者 useCallback
来缓存,只要保证引用相同即可。
“如果你的应用像此站点一样,大多数交互是粗略的(例如直接替换页面或整个部分),那么通常不需要记忆化。”
这段话在 React 官网出现过多次,也就是大多数情况下我们可能并不需要使用 memo
、useMemo
、useCallback
来帮我们做缓存。在子组件渲染成本比较高(比如图表绘制等。)的情况下再考虑使用memo
、useMemo
、useCallback
也不迟。
转载自:https://juejin.cn/post/7371820718462894131