近期,在我的维护工作中,我注意到 React Hooks 中的memo
、useMemo
和useCallback
被广泛应用,但却缺乏明确区分,在代码中随处可见。尽管从性能优化的角度来看,这些 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
引用相同 我们可以直接使用同一个变量,这个变量可以是函数外定义的同一个变量或者使用 useStateuseMemouseCallback
缓存的变量。
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} /> </>; }
当然也可以使用 useMemouseCallback
,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 的作用。此外,并非必须使用useMemo
或useCallback
来缓存传递给子组件的props
,只需要确保引用相同即可。 根据 React 官网的多次提及,如果你的应用程序像该站点一样,主要是进行粗略的交互(例如直接替换页面或整个部分),通常不需要记忆化。 对于子组件渲染成本较高的情况(例如图表绘制等),考虑使用 memo、useMemo 或 useCallback 也是可行的选择。
参考链接:memo、useCallback