useMemo到底有没有用?我差点被面试官问懵了!
在开发 React 应用时,性能优化是一个不可忽视的环节。React 提供了多个工具来帮助我们避免不必要的渲染和计算,其中 React.memo、useMemo 和 useCallback 是三个非常关键的优化手段。搞懂它们三非常有必要!!!
一、React.memo
-
当有两个组件,父组件
App,子组件Button。父组件中有两个状态count、buttonCount,子组件依赖于buttonCount,父组件依赖于count,每次父组件App状态count发生更新的时候,子组件Button也会跟着重新渲染,但它的内容根本没有变化(它依赖于buttonCount)。这样会造成不必要的渲染,影响性能。使用React.memo便可以解决这个问题。原始父子组件传参示例:
function App() { console.log("App组件渲染"); const [count, setCount] = useState(0); const [buttonCount, setButtonCount] = useState(0); return ( <> <button onClick={() => setCount(count + 1)}>App组件计数 {count}</button> <Button buttonCount={buttonCount} /> </> ); } const Button = ({ buttonCount }) => { console.log("Button组件渲染"); return ( <> <button>Button组件计数 {buttonCount}</button> </> ); }; -
什么是 React.memo?
React.memo是一个高阶组件(HOC),用于避免函数组件在 props 没有变化时的重复渲染。它通过浅比较 props 来决定是否跳过本次渲染。使用
React.memo包裹你想要优化的函数式组件即可实现const MyComponent = React.memo(function MyComponent(props) { // 只有当 props 变化时才会重新渲染 return <div>{props.value}</div>; });示例
function App() { console.log("App组件渲染"); const [count, setCount] = useState(0); const [buttonCount, setButtonCount] = useState(0); return ( <> <button onClick={() => setCount(count + 1)}>App组件计数 {count}</button> <ButtonMemo buttonCount={buttonCount} /> </> ); } const Button = ({ buttonCount }) => { console.log("Button组件渲染"); return ( <> <button>Button组件计数 {buttonCount}</button> </> ); }; const ButtonMemo = memo(Button); -
React.memo 浅比较(shallow compare)组件的
props的规则。- 如果 props 是基本类型(如
string、number、boolean),比较值是否一样。 - 如果 props 是对象、数组或函数,只会比较引用地址(浅比较),不会深入比较里面的值。由于这个对象可能定义在父组件中,每次父组件更新时这个对象就会被重新声明,其引用地址会发生变化。这会导致子组件的重新渲染。
可以通过自定义比较函数来解决这个问题
function UserCard(props) { return <div>用户名:{props.user.name}</div>; } function areEqual(prevProps, nextProps) { // 如果 name 没变,就不重新渲染 return prevProps.user.name === nextProps.user.name; } export default React.memo(UserCard, areEqual); - 如果 props 是基本类型(如
-
适用场景
- 组件接收的 props 是稳定的数据(例如来自
useMemo或useCallback)。 - 组件渲染开销较大,例如包含大量 DOM 或复杂计算。
- 父组件频繁更新,但子组件的 props 没有变化。
- 组件接收的 props 是稳定的数据(例如来自
二、useMemo
-
什么是 useMemo?
useMemo是一个 Hook,用于缓存计算结果,具体来说,useMemo通过记忆计算结果来避免在每次渲染时都进行高开销的计算,只有当依赖项发生变化时,才会重新计算。使用方式
useMemo( function , dev ),第一个参数是一个函数(开销大的函数),该函数返回需要记忆的计算结果。如下
computeExpensiveValue是一个模拟的高开销函数。通过使用useMemo,我们可以确保这个函数只在a或b变化时才执行,而不是在组件的每一次渲染过程中都执行。import React, { useMemo } from 'react'; function ExpensiveComponent({ a, b }) { // 使用 useMemo 来记忆计算结果 const memoizedValue = useMemo(() => computeExpensiveValue(a, b), [a, b]); return <div>{memoizedValue}</div>; } function computeExpensiveValue(a, b) { console.log('start expensive computation...'); // 模拟一个耗时操作 let result = 0; for (let i = 0; i < 100000000; i++) { result += a + b; } return result; }不使用useMemo:
function ExpensiveComponent({ a, b }) { // 使用 useMemo 来记忆计算结果 const memoizedValue = computeExpensiveValue(a, b) return <div>{memoizedValue}</div>; }
使用useMemo:
```jsx
function ExpensiveComponent({ a, b }) {
// 使用 useMemo 来记忆计算结果
const memoizedValue = useMemo(() => computeExpensiveValue(a, b), [a, b]);
return <div>{memoizedValue}</div>;
}
```
-
useMemo适用场景
- 计算密集型操作,例如数据转换、排序、过滤等。
- 需要将计算结果作为 props 传递给优化后的子组件(如
React.memo)。
三、useCallback
-
什么是 useCallback?
在 React 中,每次组件重新渲染时,函数组件中的函数都会重新定义,也就是说它们的引用地址会变。
例如:
function App() { const handleClick = () => { console.log("点击了"); }; return <ChildComponent onClick={handleClick} />; }在这个例子中,每次
App重新渲染,handleClick都会是一个新的函数引用。如果ChildComponent用了React.memo来优化渲染,但由于onClick是一个新的函数,React 会认为 props 改变了,于是子组件还是会重新渲染 —— 即使逻辑上它并不需要重新渲染。useCallback可以解决这个问题。useCallback是一个 Hook,用于缓存函数的引用,只有在依赖项变化时才生成新的函数。所有它常用于避免子组件因函数 props 变化而不必要的重新渲染。useCallback( 函数, 依赖项)
const memoizedCallback = useCallback(() => { doSomething(a, b); }, [a, b]); -
对比有无
useCallback没有用
useCallback的情况:const ParentComponent = () => { const [count, setCount] = useState(0); const handleClick = () => { console.log("点击了"); }; return ( <div> <button onClick={() => setCount(count + 1)}>增加</button> <ChildComponent onClick={handleClick} /> </div> ); };每次点击按钮,
handleClick都是一个新函数,即使内容没变,也会导致ChildComponent重新渲染。使用
useCallback后:const ParentComponent = () => { const [count, setCount] = useState(0); const handleClick = useCallback(() => { console.log("点击了"); }, []); return ( <div> <button onClick={() => setCount(count + 1)}>增加</button> <ChildComponent onClick={handleClick} /> </div> ); };此时,
handleClick的引用不变,如果ChildComponent用了React.memo,它就不会在count改变时重新渲染。 -
适用场景
- 将函数作为 props 传给
React.memo优化过的子组件。 - 函数作为依赖项传入其他 Hook(如
useEffect、useMemo)中,避免不必要的副作用触发。
- 将函数作为 props 传给
四、三者结合使用示例
下面是一个结合 React.memo、useMemo 和 useCallback 的完整示例:
import React, { useState, useMemo, useCallback } from 'react';
// 优化后的子组件
const MemoizedChild = React.memo(({ data, onClick }) => {
console.log('Child rendered');
return (
<div>
<h3>Memoized Child</h3>
<p>{data}</p>
<button onClick={onClick}>Click Me</button>
</div>
);
});
// 父组件
const ParentComponent = () => {
const [count, setCount] = useState(0);
const [text, setText] = useState('hello');
// 缓存计算结果
const processedText = useMemo(() => {
return text.toUpperCase();
}, [text]);
// 缓存函数引用
const handleClick = useCallback(() => {
alert('Button clicked!');
}, []);
return (
<div>
<h2>Parent Component</h2>
<p>Count: {count}</p>
<button onClick={() => setCount(prev => prev + 1)}>Increment</button>
<input value={text} onChange={e => setText(e.target.value)} />
<MemoizedChild data={processedText} onClick={handleClick} />
</div>
);
};
export default ParentComponent;
分析:
processedText使用useMemo缓存了处理后的文本,避免每次渲染都调用toUpperCase()。handleClick使用useCallback缓存函数引用,确保MemoizedChild不会因函数变化而重新渲染。MemoizedChild使用React.memo,仅在data或onClick变化时才重新渲染。
五、总结
| Hook / API | 用途 | 适用场景 | 注意事项 |
|---|---|---|---|
React.memo | 避免组件重复渲染 | 子组件频繁渲染、props 稳定 | 默认浅比较,复杂对象需自定义比较函数 |
useMemo | 缓存计算结果 | 数据处理、作为 props 传给 memo 组件 | 不保证缓存,不能依赖其保持状态 |
useCallback | 缓存函数引用 | 作为 props 传给 memo 组件、作为依赖项 | 不保证引用稳定,注意依赖项完整性 |